pax_global_header 0000666 0000000 0000000 00000000064 14511615353 0014516 g ustar 00root root 0000000 0000000 52 comment=432ce00dfbe34ed623e327cd732ce3eb2e7f115f
has_scope-0.8.2/ 0000775 0000000 0000000 00000000000 14511615353 0013471 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/.github/ 0000775 0000000 0000000 00000000000 14511615353 0015031 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/.github/workflows/ 0000775 0000000 0000000 00000000000 14511615353 0017066 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/.github/workflows/test.yml 0000664 0000000 0000000 00000003261 14511615353 0020572 0 ustar 00root root 0000000 0000000 name: Test
on: [push, pull_request]
jobs:
test:
strategy:
fail-fast: false
matrix:
gemfile:
- Gemfile
- gemfiles/Gemfile-rails-main
- gemfiles/Gemfile-rails-7-0
- gemfiles/Gemfile-rails-6-1
- gemfiles/Gemfile-rails-6-0
- gemfiles/Gemfile-rails-5-2
ruby:
- '3.2'
- '3.1'
- '3.0'
- '2.7'
- '2.6'
- '2.5'
exclude:
- gemfile: Gemfile
ruby: '2.6'
- gemfile: Gemfile
ruby: '2.5'
- gemfile: gemfiles/Gemfile-rails-main
ruby: '2.6'
- gemfile: gemfiles/Gemfile-rails-main
ruby: '2.5'
- gemfile: gemfiles/Gemfile-rails-7-0
ruby: '2.6'
- gemfile: gemfiles/Gemfile-rails-7-0
ruby: '2.5'
- gemfile: gemfiles/Gemfile-rails-6-0
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-6-0
ruby: '3.1'
- gemfile: gemfiles/Gemfile-rails-5-2
ruby: '3.2'
- gemfile: gemfiles/Gemfile-rails-5-2
ruby: '3.1'
- gemfile: gemfiles/Gemfile-rails-5-2
ruby: '3.0'
- gemfile: gemfiles/Gemfile-rails-5-2
ruby: '2.7'
runs-on: ubuntu-latest
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true # runs bundle install and caches installed gems automatically
- run: bundle exec rake
has_scope-0.8.2/.gitignore 0000664 0000000 0000000 00000000035 14511615353 0015457 0 ustar 00root root 0000000 0000000 .bundle
gemfiles/*.lock
pkg/
has_scope-0.8.2/CHANGELOG.md 0000664 0000000 0000000 00000002774 14511615353 0015314 0 ustar 00root root 0000000 0000000 ## 0.8.2
* Add support for Rails 7.1. (no changes required.)
* Add `HasScope.deprecator` to integrate with new application deprecators in Rails 7.1.
## 0.8.1
* Add support for Rails 7.0 and Ruby 3.1/3.2 (no changes required)
* Remove test files from the gem package.
* Expand readme with information on how to use the library outside of controllers.
## 0.8.0
* Fix usage of `:in` when defined with a `:default` option that is not a hash, to apply the default when param is not given.
* Fix usage of `:in` incorrectly calling scopes when receiving a blank param value without `allow_blank` set.
* Deprecate passing a String to `if` and `unless` options, in order to stop using `eval` in code.
* Require `active_support` and `action_controller` explicitly to prevent possible `uninitialized constant` errors.
* Add support for Ruby 2.7 and 3.0, drop support for Ruby < 2.5.
* Add support for Rails 6.1, drop support for Rails < 5.2.
* Move CI to GitHub Actions.
## 0.7.2
* Added support Rails 5.2 and 6.0.
## 0.7.1
* Added support Rails 5.1.
## 0.7.0
* Added support Rails 5.
* Removed support for Rails `3.2` and `4.0` and Ruby `1.9.3` and `2.0.0`.
## 0.6.0
* Allow custom types and parsers
* Boolean scopes with `allow_blank: true` are called with values, working as any other scopes
* Add `:in` option: a shortcut for combining the `:using` option with nested hashes
* Support Rails 4.1 & 4.2, Ruby 2.2
## 0.6.0.rc
* Drop support for Rails 3.1 and Ruby 1.8, keep support for Rails 3.2
* Support for Rails 4.0 onward
has_scope-0.8.2/Gemfile 0000664 0000000 0000000 00000000145 14511615353 0014764 0 ustar 00root root 0000000 0000000 source 'https://rubygems.org'
gemspec
gem "actionpack", "~> 7.1.0"
gem "activesupport", "~> 7.1.0"
has_scope-0.8.2/Gemfile.lock 0000664 0000000 0000000 00000003447 14511615353 0015723 0 ustar 00root root 0000000 0000000 PATH
remote: .
specs:
has_scope (0.8.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
GEM
remote: https://rubygems.org/
specs:
actionpack (7.1.0)
actionview (= 7.1.0)
activesupport (= 7.1.0)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actionview (7.1.0)
activesupport (= 7.1.0)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activesupport (7.1.0)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
base64 (0.1.1)
bigdecimal (3.1.4)
builder (3.2.4)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crass (1.0.6)
drb (2.1.1)
ruby2_keywords
erubi (1.12.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
loofah (2.21.4)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mini_portile2 (2.8.4)
minitest (5.20.0)
mocha (2.1.0)
ruby2_keywords (>= 0.0.5)
mutex_m (0.1.2)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
racc (1.7.1)
rack (3.0.8)
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rake (13.0.6)
ruby2_keywords (0.0.5)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
PLATFORMS
ruby
DEPENDENCIES
actionpack (~> 7.1.0)
activesupport (~> 7.1.0)
has_scope!
mocha
rake
BUNDLED WITH
2.4.20
has_scope-0.8.2/MIT-LICENSE 0000664 0000000 0000000 00000002147 14511615353 0015131 0 ustar 00root root 0000000 0000000 Copyright (c) 2020-2023 Rafael França, Carlos Antônio da Silva
Copyright (c) 2009-2019 Plataformatec
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.
has_scope-0.8.2/README.md 0000664 0000000 0000000 00000024214 14511615353 0014753 0 ustar 00root root 0000000 0000000 ## HasScope
[](http://badge.fury.io/rb/has_scope)
_HasScope_ allows you to dynamically apply named scopes to your resources based on an incoming set of parameters.
The most common usage is to map incoming controller parameters to named scopes for filtering resources, but it can be used anywhere.
## Installation
Add `has_scope` to your bundle
```ruby
bundle add has_scope
```
or add it manually to your Gemfile if you prefer.
```ruby
gem 'has_scope'
```
## Examples
For the following examples we'll use a 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
```
### Usage 1: Rails Controllers
_HasScope_ exposes the `has_scope` method automatically in all your controllers. This is used to declare the scopes a controller action can use to filter a resource:
```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
end
```
To apply the scopes to a 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 to the `index` action, _HasScope_ will automatically apply the scopes as follows:
``` ruby
# GET /graduations
# No scopes applied
#=> brings all graduations
apply_scopes(Graduation).all == Graduation.all
# GET /graduations?featured=true
# The "featured' scope is applied
#=> brings featured graduations
apply_scopes(Graduation).all == Graduation.featured
# GET /graduations?by_period[started_at]=20100701&by_period[ended_at]=20101013
#=> brings graduations in the given period
apply_scopes(Graduation).all == Graduation.by_period('20100701', '20101013')
# GET /graduations?featured=true&by_degree=phd
#=> brings featured graduations with phd degree
apply_scopes(Graduation).all == Graduation.featured.by_degree('phd')
# GET /graduations?finished=true&by_degree=phd
#=> brings only graduations with phd degree because we didn't declare finished in our controller as a permitted scope
apply_scopes(Graduation).all == Graduation.by_degree('phd')
```
#### Check for currently applied scopes
_HasScope_ creates a helper method called `current_scopes` to retrieve all the scopes applied. As it's a helper method, you'll be able to access it in the controller action or the view rendered in that action.
Coming back to one of the examples above:
```ruby
# GET /graduations?featured=true&by_degree=phd
#=> brings featured graduations with phd degree
apply_scopes(Graduation).all == Graduation.featured.by_degree('phd')
```
Calling `current_scopes` after `apply_scopes` in the controller action or view would return the following:
```
current_scopes
#=> { featured: true, by_degree: 'phd' }
```
### Usage 2: Standalone Mode
_HasScope_ can also be used in plain old Ruby objects (PORO). To implement the previous example using this approach, create a bare object and include `HasScope` to get access to its features:
> Note: We'll create a simple version of a query object for this example as this type of object can have multiple different implementations.
```ruby
class GraduationsSearchQuery
include HasScope
# ...
end
```
Next, declare the scopes to be used the same way:
```ruby
class GraduationsSearchQuery
include HasScope
has_scope :featured, type: :boolean
has_scope :by_degree
has_scope :by_period, using: %i[started_at ended_at], type: :hash
# ...
end
```
Now, allow your object to perform the query by exposing a method that will use `apply_scopes`:
```ruby
class GraduationsSearchQuery
include HasScope
has_scope :featured, type: :boolean
has_scope :by_degree
has_scope :by_period, using: %i[started_at ended_at], type: :hash
def perform(collection: Graduation, params: {})
apply_scopes(collection, params)
end
end
```
Note that `apply_scopes` receives a `Hash` as a second argument, which represents the incoming params that determine which scopes should be applied to the model/collection. It defaults to `params` for compatibility with controllers, which is why it's not necessary to pass that second argument in the controller context.
Now in your controller you can call the `GraduationsSearchQuery` with the incoming parameters from the controller:
```ruby
class GraduationsController < ApplicationController
def index
graduations_query = GraduationsSearchQuery.new
@graduations = graduations_query.perform(collection: Graduation, params: params)
end
end
```
#### Accessing `current_scopes`
In the controller context, `current_scopes` is made available as a helper method to the controller and view, but it's a `protected` method of _HasScope_'s implementation, to prevent it from becoming publicly accessible outside of _HasScope_ itself. This means that the object implementation showed above has access to `current_scopes` internally, but it's not exposed to other objects that interact with it.
If you need to access `current_scopes` elsewhere, you can change the method visibility like so:
```ruby
class GraduationsSearchQuery
include HasScope
# ...
public :current_scopes
# ...
end
```
## Options
`has_scope` 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 in case we need to manipulate the given value and/or call the scope in some custom way. Usually three arguments are passed to the block:
- The instance of the controller or object where it's included
- The current scope chain
- The value of the scope to apply
> 💡 We suggest you name the first argument depending on how you're using _HasScope_. If it's the controller, use the word "controller". If it's a query object for example, use "query", or something meaningful for that context (or simply use "context"). In the following examples, we'll use controller for simplicity.
```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.
## License
MIT License. Copyright 2020-2023 Rafael França, Carlos Antônio da Silva. Copyright 2009-2019 Plataformatec.
has_scope-0.8.2/Rakefile 0000664 0000000 0000000 00000001070 14511615353 0015134 0 ustar 00root root 0000000 0000000 # encoding: UTF-8
require 'bundler/gem_tasks'
require 'rake/testtask'
require 'rdoc/task'
desc 'Default: run unit tests.'
task default: :test
desc 'Test HasScope'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for HasScope'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'HasScope'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.md')
rdoc.rdoc_files.include('lib/**/*.rb')
end
has_scope-0.8.2/gemfiles/ 0000775 0000000 0000000 00000000000 14511615353 0015264 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/gemfiles/Gemfile-rails-5-2 0000664 0000000 0000000 00000000161 14511615353 0020166 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gem "actionpack", "~> 5.2.0"
gem "activesupport", "~> 5.2.0"
gemspec path: "../"
has_scope-0.8.2/gemfiles/Gemfile-rails-6-0 0000664 0000000 0000000 00000000161 14511615353 0020165 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gem "actionpack", "~> 6.0.0"
gem "activesupport", "~> 6.0.0"
gemspec path: "../"
has_scope-0.8.2/gemfiles/Gemfile-rails-6-1 0000664 0000000 0000000 00000000161 14511615353 0020166 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gem "actionpack", "~> 6.1.0"
gem "activesupport", "~> 6.1.0"
gemspec path: "../"
has_scope-0.8.2/gemfiles/Gemfile-rails-7-0 0000664 0000000 0000000 00000000160 14511615353 0020165 0 ustar 00root root 0000000 0000000 source 'https://rubygems.org'
gemspec path: ".."
gem "actionpack", "~> 7.0.0"
gem "activesupport", "~> 7.0.0"
has_scope-0.8.2/gemfiles/Gemfile-rails-main 0000664 0000000 0000000 00000000247 14511615353 0020614 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gem 'actionpack', github: 'rails/rails', branch: 'main'
gem 'activesupport', github: 'rails/rails', branch: 'main'
gemspec path: "../"
has_scope-0.8.2/has_scope.gemspec 0000664 0000000 0000000 00000002375 14511615353 0017011 0 ustar 00root root 0000000 0000000 # -*- encoding: utf-8 -*-
$:.push File.expand_path('../lib', __FILE__)
require 'has_scope/version'
Gem::Specification.new do |s|
s.name = 'has_scope'
s.version = HasScope::VERSION.dup
s.platform = Gem::Platform::RUBY
s.summary = 'Maps controller filters to your resource scopes.'
s.email = 'opensource@plataformatec.com.br'
s.homepage = 'http://github.com/plataformatec/has_scope'
s.description = 'Maps controller filters to your resource scopes'
s.authors = ['José Valim']
s.license = 'MIT'
s.metadata = {
"homepage_uri" => "https://github.com/heartcombo/has_scope",
"changelog_uri" => "https://github.com/heartcombo/has_scope/blob/main/CHANGELOG.md",
"source_code_uri" => "https://github.com/heartcombo/has_scope",
"bug_tracker_uri" => "https://github.com/heartcombo/has_scope/issues",
}
s.files = Dir['MIT-LICENSE', 'README.md', 'lib/**/*']
s.require_paths = ['lib']
s.rdoc_options = ['--charset=UTF-8']
s.extra_rdoc_files = [
'README.md'
]
s.required_ruby_version = '>= 2.5.0'
s.add_runtime_dependency 'actionpack', '>= 5.2'
s.add_runtime_dependency 'activesupport', '>= 5.2'
s.add_development_dependency 'rake'
s.add_development_dependency 'mocha'
end
has_scope-0.8.2/lib/ 0000775 0000000 0000000 00000000000 14511615353 0014237 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/lib/has_scope.rb 0000664 0000000 0000000 00000016753 14511615353 0016544 0 ustar 00root root 0000000 0000000 require '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.deprecator
@deprecator ||= ActiveSupport::Deprecation.new("1.0", "HasScope")
end
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
HasScope.deprecator.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
require 'has_scope/railtie' if defined?(Rails)
ActiveSupport.on_load :action_controller do
include HasScope
helper_method :current_scopes if respond_to?(:helper_method)
end
has_scope-0.8.2/lib/has_scope/ 0000775 0000000 0000000 00000000000 14511615353 0016203 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/lib/has_scope/railtie.rb 0000664 0000000 0000000 00000000346 14511615353 0020164 0 ustar 00root root 0000000 0000000 require 'rails/railtie'
module HasScope
class Railtie < Rails::Railtie
initializer "has_scope.deprecator" do |app|
app.deprecators[:has_scope] = HasScope.deprecator if app.respond_to?(:deprecators)
end
end
end
has_scope-0.8.2/lib/has_scope/version.rb 0000664 0000000 0000000 00000000050 14511615353 0020210 0 ustar 00root root 0000000 0000000 module HasScope
VERSION = "0.8.2"
end
has_scope-0.8.2/test/ 0000775 0000000 0000000 00000000000 14511615353 0014450 5 ustar 00root root 0000000 0000000 has_scope-0.8.2/test/has_scope_test.rb 0000664 0000000 0000000 00000040365 14511615353 0020010 0 ustar 00root root 0000000 0000000 require '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
HasScope.deprecator.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_sequence = sequence('tree')
Tree.expects(:only_tall).with().returns(Tree).in_sequence(tree_sequence)
Tree.expects(:all).returns([mock_tree]).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:conifer).with(true).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:all).returns([mock_tree]).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:conifer).with(false).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:all).returns([mock_tree]).in_sequence(tree_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/, HasScope.deprecator) 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_sequence = sequence('tree')
Tree.expects(:color).with('blue').returns(Tree).in_sequence(tree_sequence)
Tree.expects(:all).returns([mock_tree]).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:color).never
Tree.expects(:all).returns([mock_tree]).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:root_type).with('').returns(Tree)
Tree.expects(:all).returns([mock_tree]).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:shadown_range).with(10).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:paginate_default).with({ 'page' => 1, 'per_page' => 10 }).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:args_paginate_default).with(1, 10).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:metadata_default).with('default').returns(Tree).in_sequence(tree_sequence)
Tree.expects(:find).with('42').returns(mock_tree).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:shadown_range).with('20').returns(Tree).in_sequence(tree_sequence)
Tree.expects(:paginate_default).with({ 'page' => '2', 'per_page' => '20' }).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:args_paginate_default).with('3', '15').returns(Tree).in_sequence(tree_sequence)
Tree.expects(:metadata_blank).with(nil).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:metadata_default).with('other').returns(Tree).in_sequence(tree_sequence)
Tree.expects(:find).with('42').returns(mock_tree).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:root_type).with('outside').returns(Tree).in_sequence(tree_sequence)
Tree.expects(:find).with('42').returns(mock_tree).in_sequence(tree_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_sequence = sequence('tree')
Tree.expects(:calculate_height).with(100).returns(Tree).in_sequence(tree_sequence)
Tree.expects(:new).returns(mock_tree).in_sequence(tree_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.2/test/test_helper.rb 0000664 0000000 0000000 00000001111 14511615353 0017305 0 ustar 00root root 0000000 0000000 require 'bundler/setup'
require 'minitest/autorun'
require 'mocha'
require 'mocha/minitest'
# 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