mirror of
https://github.com/activerecord-hackery/ransack.git
synced 2022-11-09 13:47:45 -05:00
merged travis.yml, Gemfile and Context
This commit is contained in:
commit
aeeaf597f6
15 changed files with 473 additions and 166 deletions
|
@ -7,9 +7,13 @@ sudo: false
|
|||
rvm:
|
||||
- 2.1
|
||||
- 2.0
|
||||
- 1.9.3
|
||||
- 1.9
|
||||
|
||||
env:
|
||||
- RAILS=master DB=mongodb
|
||||
- RAILS=master DB=sqlite3
|
||||
- RAILS=master DB=mysql
|
||||
- RAILS=master DB=postgres
|
||||
- RAILS=4-1-stable DB=mongodb
|
||||
- RAILS=4-1-stable DB=sqlite3
|
||||
- RAILS=4-1-stable DB=mysql
|
||||
|
|
62
CHANGELOG.md
62
CHANGELOG.md
|
@ -1,27 +1,67 @@
|
|||
# Change Log
|
||||
All notable changes to this project from August 2014 on will be documented here.
|
||||
This change log was started in August 2014. All notable changes to this project
|
||||
henceforth should be documented here.
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
|
||||
* Add `ro.yml` Romanian translation file.
|
||||
* `sort_link` helper: Add support for multiple sort fields and default orders
|
||||
([pull request](https://github.com/activerecord-hackery/ransack/pull/438)).
|
||||
|
||||
*Andreas Philippi*
|
||||
*Caleb Land*, *James u007*
|
||||
|
||||
### Fixed
|
||||
|
||||
### Changed
|
||||
|
||||
* Reduce object allocations and memory footprint (with a slight speed gain as
|
||||
well) by extracting commonly used strings into top level constants and
|
||||
replacing calls to `#try` methods with simple nil checking.
|
||||
|
||||
*Jon Atack*
|
||||
|
||||
|
||||
## Version 1.4.1 - 2014-09-23
|
||||
### Fixed
|
||||
|
||||
* Fix README markdown so RubyGems documentation picks up the formatting correctly.
|
||||
|
||||
*Jon Atack*
|
||||
|
||||
|
||||
## Version 1.4.0 - 2014-09-23
|
||||
### Added
|
||||
|
||||
* Add support for Rails 4.2.0! Let us know if you encounter any issues.
|
||||
|
||||
*Xiang Li*
|
||||
|
||||
* Add `not_true` and `not_false` predicates and update the "Basic Searching"
|
||||
wiki. Fixes #123, #353.
|
||||
|
||||
*Pedro Chambino*
|
||||
|
||||
* Start a CHANGELOG.
|
||||
* Add `ro.yml` Romanian translation file.
|
||||
|
||||
*Andreas Philippi*
|
||||
|
||||
* Add new documentation in the README explaining how to group queries by `OR`
|
||||
instead of the default `AND` using the `m: 'or'` combinator.
|
||||
|
||||
* Add new documentation in the README and in the source code comments
|
||||
explaining in detail how to handle whitelisting/authorization of
|
||||
attributes, associations, sorts and scopes.
|
||||
|
||||
* Add new documentation in the README explaining in more detail how to use
|
||||
scopes for searching with Ransack.
|
||||
|
||||
* Begin a CHANGELOG.
|
||||
|
||||
*Jon Atack*
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix attribute translations when using ActiveRecord with STI.
|
||||
* Fix singular/plural Active Record attribute translations.
|
||||
|
||||
*Andreas Philippi*
|
||||
|
||||
|
@ -29,7 +69,7 @@ All notable changes to this project from August 2014 on will be documented here.
|
|||
|
||||
*Daniel Rikowski*
|
||||
|
||||
* Apply default scope conditions for association joins (Rails 3).
|
||||
* Apply default scope conditions for association joins (fix for Rails 3).
|
||||
|
||||
Avoid selecting records from joins that would normally be filtered out
|
||||
if they were selected from the base table. Only applies to Rails 3, as
|
||||
|
@ -37,8 +77,8 @@ All notable changes to this project from August 2014 on will be documented here.
|
|||
|
||||
*Andrew Vit*
|
||||
|
||||
* Fix incoherent code examples in the README Associations section that mixed
|
||||
up `@q` and `@search`.
|
||||
* Fix incoherent code examples in the README Associations section that
|
||||
sometimes used `@q` and other times `@search`.
|
||||
|
||||
*Jon Atack*
|
||||
|
||||
|
@ -46,9 +86,9 @@ All notable changes to this project from August 2014 on will be documented here.
|
|||
|
||||
* Refactor Ransack::Translate.
|
||||
|
||||
* Rewrite much of the README doc, including the Associations section
|
||||
code examples and the Authorizations section showing how to whitelist
|
||||
attributes, associations, sorts and scopes.
|
||||
* Rewrite much of the Ransack README documentation, including the
|
||||
Associations section code examples and the Authorizations section detailing
|
||||
how to whitelist attributes, associations, sorts and scopes.
|
||||
|
||||
*Jon Atack*
|
||||
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -3,12 +3,15 @@ gemspec
|
|||
|
||||
gem 'rake'
|
||||
|
||||
rails = ENV['RAILS'] || '4-1-stable'
|
||||
rails = ENV['RAILS'] || 'master'
|
||||
|
||||
gem 'polyamorous', '~> 1.1'
|
||||
|
||||
gem 'pry'
|
||||
|
||||
# Provide timezone information on Windows
|
||||
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw]
|
||||
|
||||
case rails
|
||||
when /\// # A path
|
||||
gem 'activesupport', path: "#{rails}/activesupport"
|
||||
|
|
157
README.md
157
README.md
|
@ -26,47 +26,34 @@ instead.
|
|||
|
||||
## Getting started
|
||||
|
||||
Because ActiveRecord has been evolving quite a bit, your friendly Ransack is
|
||||
available in several flavors! Take your pick:
|
||||
Ransack is currently compatible with Rails 3.x, 4.0, 4.1 and 4.2.
|
||||
|
||||
In your Gemfile, for the last officially released gem compatible with Rails
|
||||
3.x, 4.0 and 4.1 (for Rails 4.2, use the dedicated `rails-4.2` branch described
|
||||
below for now):
|
||||
In your Gemfile, for the last officially released Ransack gem:
|
||||
|
||||
```ruby
|
||||
gem 'ransack'
|
||||
```
|
||||
|
||||
Or if you want to use the latest updates on the Ransack master branch:
|
||||
Or, if you would like to use the latest updates:
|
||||
|
||||
```ruby
|
||||
gem 'ransack', github: 'activerecord-hackery/ransack'
|
||||
```
|
||||
|
||||
If you are using Rails 4.1, you may prefer the dedicated [Rails 4.1 branch]
|
||||
(https://github.com/activerecord-hackery/ransack/tree/rails-4.1) which
|
||||
usually contains the latest updates on master (albeit sometimes with some
|
||||
delay), supports only 4.1, and is lighter and somewhat faster:
|
||||
The other branches (`rails-4`, `rails-4.1`, and `rails-4.2`) were each used for
|
||||
developing and running Ransack with the latest upcoming version of Rails at the
|
||||
time. They are smaller and possibly slightly faster because they do not have to
|
||||
support previous versions of Rails and Active Record. Once support for that
|
||||
Rails version is merged from the branch into Ransack master, the branch is no
|
||||
longer actively maintained -- unless the open source community submits pull
|
||||
requests to maintain them. You are welcome to do so!
|
||||
|
||||
To use one of the branches, for example the `rails-4.1` branch:
|
||||
|
||||
```ruby
|
||||
gem 'ransack', github: 'activerecord-hackery/ransack', branch: 'rails-4.1'
|
||||
```
|
||||
|
||||
Similarly, if you are using Rails 4.0, you may prefer the dedicated
|
||||
[Rails 4 branch](https://github.com/activerecord-hackery/ransack/tree/rails-4)
|
||||
for the same reasons:
|
||||
|
||||
```ruby
|
||||
gem 'ransack', github: 'activerecord-hackery/ransack', branch: 'rails-4'
|
||||
```
|
||||
|
||||
Last but definitely not least, an experimental [Rails 4.2 branch]
|
||||
(https://github.com/activerecord-hackery/ransack/tree/rails-4.2) is available:
|
||||
|
||||
```ruby
|
||||
gem 'ransack', github: 'activerecord-hackery/ransack', branch: 'rails-4.2'
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Ransack can be used in one of two modes, simple or advanced.
|
||||
|
@ -90,15 +77,18 @@ If you're coming from MetaSearch, things to note:
|
|||
3. Common ActiveRecord::Relation methods are no longer delegated by the
|
||||
search object. Instead, you will get your search results (an
|
||||
ActiveRecord::Relation in the case of the ActiveRecord adapter) via a call to
|
||||
`Search#result`. If passed `distinct: true`, `result` will generate a `SELECT
|
||||
DISTINCT` to avoid returning duplicate rows, even if conditions on a join
|
||||
would otherwise result in some.
|
||||
`Search#result`.
|
||||
|
||||
4. If passed `distinct: true`, `result` will generate a `SELECT DISTINCT` to
|
||||
avoid returning duplicate rows, even if conditions on a join would otherwise
|
||||
result in some.
|
||||
|
||||
Please note that for many databases, a sort on an associated table's columns
|
||||
may result in invalid SQL with `distinct: true` -- in those cases, you're on
|
||||
your own, and will need to modify the result as needed to allow these queries
|
||||
to work. One good workaround if `distinct: true` is causing problems, can be to
|
||||
not use it and call `#to_a.uniq` on your final collection instead.
|
||||
to work. If `distinct: true` is causing you problems, another way to remove
|
||||
duplicates is to call `#to_a.uniq` on your collection instead (see the next
|
||||
section below).
|
||||
|
||||
####In your controller
|
||||
|
||||
|
@ -126,7 +116,7 @@ The two primary Ransack view helpers are `search_form_for` and `sort_link`,
|
|||
which are defined in
|
||||
[Ransack::Helpers::FormHelper](lib/ransack/helpers/form_helper.rb).
|
||||
|
||||
#####1. Ransack's `search_form_for` helper replaces `form_for` for creating the view search form:
|
||||
#####Ransack's `search_form_for` helper replaces `form_for` for creating the view search form:
|
||||
|
||||
```erb
|
||||
<%= search_form_for @q do |f| %>
|
||||
|
@ -161,18 +151,39 @@ The `search_form_for` answer format can be set like this:
|
|||
<%= search_form_for(@q, format: :json) do |f| %>
|
||||
```
|
||||
|
||||
#####2. Ransack's `sort_link` helper creates table headers that are sortable links:
|
||||
#####Ransack's `sort_link` helper creates table headers that are sortable links:
|
||||
|
||||
```erb
|
||||
<%= content_tag :th, sort_link(@q, :name) %>
|
||||
<%= sort_link(@q, :name) %>
|
||||
```
|
||||
Additional options can be passed after the column attribute, like a different
|
||||
column title or a default sort order:
|
||||
|
||||
```erb
|
||||
<%= content_tag :th, sort_link(@q, :name, 'Last Name', default_order: :desc) %>
|
||||
<%= sort_link(@q, :name, 'Last Name', default_order: :desc) %>
|
||||
```
|
||||
|
||||
You can also sort on multiple fields by specifying an ordered array:
|
||||
|
||||
```erb
|
||||
<%= sort_link(@q, :last_name, [:last_name, 'first_name asc'], 'Last Name') %>
|
||||
```
|
||||
|
||||
In the example above, clicking the link will sort by `last_name` and then
|
||||
`first_name`. Specifying the sort direction on a field in the array tells
|
||||
Ransack to _always_ sort that particular field in the specified direction.
|
||||
|
||||
Multiple `default_order` fields may also be specified with a hash:
|
||||
|
||||
```erb
|
||||
<%= sort_link(@q, :last_name, [:last_name, :first_name],
|
||||
default_order: { last_name: 'asc', first_name: 'desc' }) %>
|
||||
```
|
||||
|
||||
This example toggles the sort directions of both fields, by default
|
||||
initially sorting the `last_name` field by ascending order, and the
|
||||
`first_name` field by descending order.
|
||||
|
||||
### Advanced Mode
|
||||
|
||||
"Advanced" searches (ab)use Rails' nested attributes functionality in order to
|
||||
|
@ -307,29 +318,37 @@ class methods in your models to apply selective authorization:
|
|||
Here is how these four methods are implemented in Ransack:
|
||||
|
||||
```ruby
|
||||
def ransackable_attributes(auth_object = nil)
|
||||
# By default returns all column names and any defined ransackers as strings.
|
||||
# For overriding with a whitelist of strings.
|
||||
column_names + _ransackers.keys
|
||||
end
|
||||
# Ransackable_attributes, by default, returns all column names
|
||||
# and any defined ransackers as an array of strings.
|
||||
# For overriding with a whitelist array of strings.
|
||||
#
|
||||
def ransackable_attributes(auth_object = nil)
|
||||
column_names + _ransackers.keys
|
||||
end
|
||||
|
||||
def ransackable_associations(auth_object = nil)
|
||||
# By default returns the names of all associations as strings.
|
||||
# For overriding with a whitelist of strings.
|
||||
reflect_on_all_associations.map { |a| a.name.to_s }
|
||||
end
|
||||
# Ransackable_associations, by default, returns the names
|
||||
# of all associations as an array of strings.
|
||||
# For overriding with a whitelist array of strings.
|
||||
#
|
||||
def ransackable_associations(auth_object = nil)
|
||||
reflect_on_all_associations.map { |a| a.name.to_s }
|
||||
end
|
||||
|
||||
def ransortable_attributes(auth_object = nil)
|
||||
# By default returns the names of all attributes for sorting.
|
||||
# For overriding with a whitelist of strings.
|
||||
ransackable_attributes(auth_object)
|
||||
end
|
||||
# Ransortable_attributes, by default, returns the names
|
||||
# of all attributes available for sorting as an array of strings.
|
||||
# For overriding with a whitelist array of strings.
|
||||
#
|
||||
def ransortable_attributes(auth_object = nil)
|
||||
ransackable_attributes(auth_object)
|
||||
end
|
||||
|
||||
def ransackable_scopes(auth_object = nil)
|
||||
# By default returns an empty array, i.e. no class methods/scopes
|
||||
# are authorized. For overriding with a whitelist of *symbols*.
|
||||
[]
|
||||
end
|
||||
# Ransackable_scopes, by default, returns an empty array
|
||||
# i.e. no class methods/scopes are authorized.
|
||||
# For overriding with a whitelist array of *symbols*.
|
||||
#
|
||||
def ransackable_scopes(auth_object = nil)
|
||||
[]
|
||||
end
|
||||
```
|
||||
|
||||
Any values not returned from these methods will be ignored by Ransack, i.e.
|
||||
|
@ -345,8 +364,8 @@ Here is an example that puts all this together, adapted from
|
|||
(http://erniemiller.org/2012/05/11/why-your-ruby-class-macros-might-suck-mine-did/).
|
||||
In an `Article` model, add the following `ransackable_attributes` class method
|
||||
(preferably private):
|
||||
|
||||
```ruby
|
||||
# article.rb
|
||||
class Article < ActiveRecord::Base
|
||||
|
||||
private
|
||||
|
@ -362,9 +381,10 @@ class Article < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
```
|
||||
|
||||
Here is example code for the `articles_controller`:
|
||||
|
||||
```ruby
|
||||
# articles_controller.rb
|
||||
class ArticlesController < ApplicationController
|
||||
|
||||
def index
|
||||
|
@ -379,7 +399,9 @@ class ArticlesController < ApplicationController
|
|||
end
|
||||
end
|
||||
```
|
||||
|
||||
Trying it out in `rails console`:
|
||||
|
||||
```ruby
|
||||
> Article
|
||||
=> Article(id: integer, person_id: integer, title: string, body: text)
|
||||
|
@ -399,18 +421,20 @@ Trying it out in `rails console`:
|
|||
> Article.search({ id_eq: 1 }, { auth_object: :admin }).result.to_sql
|
||||
=> SELECT "articles".* FROM "articles" WHERE "articles"."id" = 1
|
||||
```
|
||||
|
||||
That's it! Now you know how to whitelist/blacklist various elements in Ransack.
|
||||
|
||||
### Using Scopes/Class Methods
|
||||
|
||||
Continuing on from the preceding section, searching by scopes requires defining
|
||||
a whitelist of `ransackable_scopes` on the model class. 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:
|
||||
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:
|
||||
|
||||
```ruby
|
||||
class Employee < ActiveRecord::Base
|
||||
scope :active, ->(boolean = true) { (where active: boolean) }
|
||||
scope :active, ->(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:
|
||||
|
@ -437,6 +461,19 @@ Employee.search({ active: true, hired_since: '2013-01-01' })
|
|||
Employee.search({ salary_gt: 100_000 }, { auth_object: current_user })
|
||||
```
|
||||
|
||||
Scopes are a recent addition to Ransack and currently have a few caveats:
|
||||
First, a scope involving child associations needs to be defined in the parent
|
||||
table model, not in the child model. Second, scopes with an array as an
|
||||
argument are not easily usable yet, because the array currently needs to be
|
||||
wrapped in an array to function (see
|
||||
[this issue](https://github.com/activerecord-hackery/ransack/issues/404)),
|
||||
which is not compatible with Ransack form helpers. For this use case, it may be
|
||||
better for now to use [ransackers]
|
||||
(https://github.com/activerecord-hackery/ransack/wiki/Using-Ransackers) instead
|
||||
where feasible. Finally, there is also
|
||||
[this issue](https://github.com/activerecord-hackery/ransack/issues/403)
|
||||
to be aware of. Pull requests with solutions and tests are welcome!
|
||||
|
||||
### Grouping queries by OR instead of AND
|
||||
|
||||
The default `AND` grouping can be changed to `OR` by adding `m: 'or'` to the
|
||||
|
|
|
@ -20,27 +20,35 @@ module Ransack
|
|||
.new(self, name, opts, &block)
|
||||
end
|
||||
|
||||
# Ransackable_attributes, by default, returns all column names
|
||||
# and any defined ransackers as an array of strings.
|
||||
# For overriding with a whitelist array of strings.
|
||||
#
|
||||
def ransackable_attributes(auth_object = nil)
|
||||
# By default returns all column names and any defined ransackers
|
||||
# as strings. For overriding with a whitelist of strings.
|
||||
column_names + _ransackers.keys
|
||||
end
|
||||
|
||||
# Ransackable_associations, by default, returns the names
|
||||
# of all associations as an array of strings.
|
||||
# For overriding with a whitelist array of strings.
|
||||
#
|
||||
def ransackable_associations(auth_object = nil)
|
||||
# By default returns the names of all associations as strings.
|
||||
# For overriding with a whitelist of strings.
|
||||
reflect_on_all_associations.map { |a| a.name.to_s }
|
||||
end
|
||||
|
||||
# Ransortable_attributes, by default, returns the names
|
||||
# of all attributes available for sorting as an array of strings.
|
||||
# For overriding with a whitelist array of strings.
|
||||
#
|
||||
def ransortable_attributes(auth_object = nil)
|
||||
# By default returns the names of all attributes for sorting.
|
||||
# For overriding with a whitelist of strings.
|
||||
ransackable_attributes(auth_object)
|
||||
end
|
||||
|
||||
# Ransackable_scopes, by default, returns an empty array
|
||||
# i.e. no class methods/scopes are authorized.
|
||||
# For overriding with a whitelist array of *symbols*.
|
||||
#
|
||||
def ransackable_scopes(auth_object = nil)
|
||||
# By default returns an empty array, i.e. no class methods/scopes
|
||||
# are authorized. For overriding with a whitelist of symbols.
|
||||
[]
|
||||
end
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ module Ransack
|
|||
@object = relation_for(object)
|
||||
@klass = @object.klass
|
||||
@join_dependency = join_dependency(@object)
|
||||
@join_type = options[:join_type] || Arel::OuterJoin
|
||||
@join_type = options[:join_type] || Polyamorous::OuterJoin
|
||||
@search_key = options[:search_key] || Ransack.options[:search_key]
|
||||
|
||||
if ::ActiveRecord::VERSION::STRING >= "4.1"
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
require 'ransack/constants'
|
||||
require 'ransack/predicate'
|
||||
|
||||
ASC = 'asc'.freeze
|
||||
DESC = 'desc'.freeze
|
||||
ASC_DESC = %w(asc desc).freeze
|
||||
ASC_ARROW = '▲'.freeze
|
||||
DESC_ARROW = '▼'.freeze
|
||||
OR = 'or'.freeze
|
||||
AND = 'and'.freeze
|
||||
SORT = 'sort'.freeze
|
||||
SORT_LINK = 'sort_link'.freeze
|
||||
SUFFIXES = %w(_any _all).freeze
|
||||
ATTRIBUTE = 'attribute'.freeze
|
||||
SEARCH = 'search'.freeze
|
||||
DEFAULT_SEARCH_KEY = 'q'.freeze
|
||||
SPACE = ' '.freeze
|
||||
NON_BREAKING_SPACE = ' '.freeze
|
||||
|
||||
module Ransack
|
||||
module Configuration
|
||||
|
||||
|
@ -24,7 +40,7 @@ module Ransack
|
|||
|
||||
self.predicates[name] = Predicate.new(opts)
|
||||
|
||||
['_any', '_all'].each do |suffix|
|
||||
SUFFIXES.each do |suffix|
|
||||
compound_name = name + suffix
|
||||
self.predicates[compound_name] = Predicate.new(
|
||||
opts.merge(
|
||||
|
|
|
@ -27,7 +27,7 @@ module Ransack
|
|||
def attribute_select(options = nil, html_options = nil, action = nil)
|
||||
options = options || {}
|
||||
html_options = html_options || {}
|
||||
action = action || 'search'
|
||||
action = action || SEARCH
|
||||
default = options.delete(:default)
|
||||
raise ArgumentError, formbuilder_error_message(
|
||||
"#{action}_select") unless object.respond_to?(:context)
|
||||
|
@ -55,7 +55,7 @@ module Ransack
|
|||
end
|
||||
|
||||
def sort_select(options = {}, html_options = {})
|
||||
attribute_select(options, html_options, 'sort') +
|
||||
attribute_select(options, html_options, SORT) +
|
||||
sort_direction_select(options, html_options)
|
||||
end
|
||||
|
||||
|
@ -157,14 +157,14 @@ module Ransack
|
|||
end
|
||||
|
||||
def sort_array
|
||||
[['asc', object.translate('asc')], ['desc', object.translate('desc')]]
|
||||
[[ASC, object.translate(ASC)], [DESC, object.translate(DESC)]]
|
||||
end
|
||||
|
||||
def combinator_choices
|
||||
if Nodes::Condition === object
|
||||
[['or', Translate.word(:any)], ['and', Translate.word(:all)]]
|
||||
[[OR, Translate.word(:any)], [AND, Translate.word(:all)]]
|
||||
else
|
||||
[['and', Translate.word(:all)], ['or', Translate.word(:any)]]
|
||||
[[AND, Translate.word(:all)], [OR, Translate.word(:any)]]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -231,7 +231,7 @@ module Ransack
|
|||
end
|
||||
|
||||
def formbuilder_error_message(action)
|
||||
"#{action.sub('search', 'attribute')
|
||||
"#{action.sub(SEARCH, ATTRIBUTE)
|
||||
} must be called inside a search FormBuilder!"
|
||||
end
|
||||
|
||||
|
|
|
@ -2,26 +2,6 @@ module Ransack
|
|||
module Helpers
|
||||
module FormHelper
|
||||
|
||||
def asc
|
||||
'asc'.freeze
|
||||
end
|
||||
|
||||
def desc
|
||||
'desc'.freeze
|
||||
end
|
||||
|
||||
def asc_arrow
|
||||
'▲'.freeze
|
||||
end
|
||||
|
||||
def desc_arrow
|
||||
'▼'.freeze
|
||||
end
|
||||
|
||||
def non_breaking_space
|
||||
' '.freeze
|
||||
end
|
||||
|
||||
def search_form_for(record, options = {}, &proc)
|
||||
if record.is_a?(Ransack::Search)
|
||||
search = record
|
||||
|
@ -48,13 +28,14 @@ module Ransack
|
|||
"#{search.klass.to_s.underscore}_search",
|
||||
:method => :get
|
||||
}
|
||||
options[:as] ||= 'q'.freeze
|
||||
options[:as] ||= DEFAULT_SEARCH_KEY
|
||||
options[:html].reverse_merge!(html_options)
|
||||
options[:builder] ||= FormBuilder
|
||||
|
||||
form_for(record, options, &proc)
|
||||
end
|
||||
|
||||
# sort_link @q, :name, [:name, 'kind ASC'], 'Player Name'
|
||||
def sort_link(search, attribute, *args)
|
||||
# Extract out a routing proxy for url_for scoping later
|
||||
if search.is_a?(Array)
|
||||
|
@ -65,65 +46,106 @@ module Ransack
|
|||
raise TypeError, "First argument must be a Ransack::Search!" unless
|
||||
Search === search
|
||||
|
||||
search_params = params[search.context.search_key].presence ||
|
||||
{}.with_indifferent_access
|
||||
# This is the field that this link represents. The direction of the sort icon (up/down arrow) will
|
||||
# depend on the sort status of this field
|
||||
field_name = attribute.to_s
|
||||
|
||||
attr_name = attribute.to_s
|
||||
# Determine the fields we want to sort on
|
||||
sort_fields = if Array === args.first
|
||||
args.shift
|
||||
else
|
||||
Array(field_name)
|
||||
end
|
||||
|
||||
name = (
|
||||
if args.size > 0 && !args.first.is_a?(Hash)
|
||||
args.shift.to_s
|
||||
else
|
||||
Translate.attribute(attr_name, :context => search.context)
|
||||
end
|
||||
)
|
||||
|
||||
if existing_sort = search.sorts.detect { |s| s.name == attr_name }
|
||||
prev_attr, prev_dir = existing_sort.name, existing_sort.dir
|
||||
label_text =
|
||||
if !args.first.try(:is_a?, Hash)
|
||||
args.shift.to_s
|
||||
else
|
||||
Translate.attribute(field_name, :context => search.context)
|
||||
end
|
||||
|
||||
options = args.first.is_a?(Hash) ? args.shift.dup : {}
|
||||
default_order = options.delete :default_order
|
||||
current_dir = prev_attr == attr_name ? prev_dir : nil
|
||||
default_order_is_a_hash = Hash === default_order
|
||||
|
||||
if current_dir
|
||||
new_dir = current_dir == desc ? asc : desc
|
||||
else
|
||||
new_dir = default_order || asc
|
||||
# If the default order is a hash of fields, duplicate it and let us access it with strings or symbols
|
||||
default_order = default_order.dup.with_indifferent_access if
|
||||
default_order_is_a_hash
|
||||
|
||||
search_params = params[search.context.search_key].presence ||
|
||||
{}.with_indifferent_access
|
||||
|
||||
# Find the current direction (if there is one) of the primary sort field
|
||||
if existing_sort = search.sorts.detect { |s| s.name == field_name }
|
||||
field_current_dir = existing_sort.dir
|
||||
end
|
||||
|
||||
sort_params = []
|
||||
|
||||
Array(sort_fields).each do |sort_field|
|
||||
attr_name, new_dir = sort_field.to_s.downcase.split(/\s+/)
|
||||
current_dir = nil
|
||||
|
||||
# if the user didn't specify the sort direction, detect the previous
|
||||
# sort direction on this field and reverse it
|
||||
if ASC_DESC.none? { |d| d == new_dir }
|
||||
if existing_sort = search.sorts.detect { |s| s.name == attr_name }
|
||||
current_dir = existing_sort.dir
|
||||
end
|
||||
|
||||
new_dir =
|
||||
if current_dir
|
||||
current_dir == DESC ? ASC : DESC
|
||||
elsif default_order_is_a_hash
|
||||
default_order[attr_name] || ASC
|
||||
else
|
||||
default_order || ASC
|
||||
end
|
||||
end
|
||||
|
||||
sort_params << "#{attr_name} #{new_dir}"
|
||||
end
|
||||
|
||||
# if there is only one sort parameter, remove it from the array and just
|
||||
# use the string as the parameter
|
||||
sort_params = sort_params.first if sort_params.size == 1
|
||||
|
||||
html_options = args.first.is_a?(Hash) ? args.shift.dup : {}
|
||||
css = ['sort_link', current_dir].compact.join(' ')
|
||||
html_options[:class] = [css, html_options[:class]].compact.join(' ')
|
||||
css = [SORT_LINK, field_current_dir].compact.join(SPACE)
|
||||
html_options[:class] = [css, html_options[:class]].compact.join(SPACE)
|
||||
|
||||
query_hash = {}
|
||||
query_hash[search.context.search_key] = search_params
|
||||
.merge(:s => "#{attr_name} #{new_dir}")
|
||||
.merge(:s => sort_params)
|
||||
options.merge!(query_hash)
|
||||
options_for_url = params.merge options
|
||||
options_for_url = params.merge(options)
|
||||
|
||||
url = if routing_proxy && respond_to?(routing_proxy)
|
||||
url =
|
||||
if routing_proxy && respond_to?(routing_proxy)
|
||||
send(routing_proxy).url_for(options_for_url)
|
||||
else
|
||||
url_for(options_for_url)
|
||||
end
|
||||
|
||||
link_to(
|
||||
[ERB::Util.h(name), order_indicator_for(current_dir)]
|
||||
.compact
|
||||
.join(non_breaking_space)
|
||||
.html_safe,
|
||||
url,
|
||||
html_options
|
||||
)
|
||||
name = link_name(label_text, field_current_dir)
|
||||
|
||||
link_to(name, url, html_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def order_indicator_for(order)
|
||||
if order == asc
|
||||
asc_arrow
|
||||
elsif order == desc
|
||||
desc_arrow
|
||||
def link_name(label_text, dir)
|
||||
[ERB::Util.h(label_text), order_indicator_for(dir)]
|
||||
.compact
|
||||
.join(NON_BREAKING_SPACE)
|
||||
.html_safe
|
||||
end
|
||||
|
||||
def order_indicator_for(dir)
|
||||
if dir == ASC
|
||||
ASC_ARROW
|
||||
elsif dir == DESC
|
||||
DESC_ARROW
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -35,8 +35,8 @@ module Ransack
|
|||
end
|
||||
|
||||
def dir=(dir)
|
||||
dir = dir.try(:downcase)
|
||||
@dir = %w(asc desc).include?(dir) ? dir : 'asc'
|
||||
dir = dir.downcase if dir
|
||||
@dir = ASC_DESC.include?(dir) ? dir : ASC
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -135,8 +135,9 @@ module Ransack
|
|||
end
|
||||
|
||||
def self.translated_attribute(associated_class)
|
||||
"#{associated_class.i18n_scope}.attributes.#{
|
||||
i18n_key(associated_class)}.#{@attr_name}".to_sym
|
||||
key = "#{associated_class.i18n_scope}.attributes.#{
|
||||
i18n_key(associated_class)}.#{@attr_name}"
|
||||
["#{key}.one".to_sym, key.to_sym]
|
||||
end
|
||||
|
||||
def self.translated_ancestor_attributes
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module Ransack
|
||||
VERSION = "1.3.0"
|
||||
VERSION = "1.4.1"
|
||||
end
|
||||
|
|
|
@ -3,11 +3,16 @@ require 'spec_helper'
|
|||
module Ransack
|
||||
module Adapters
|
||||
module ActiveRecord
|
||||
version = ::ActiveRecord::VERSION
|
||||
AR_version = "#{version::MAJOR}.#{version::MINOR}"
|
||||
|
||||
describe Context do
|
||||
subject { Context.new(Person) }
|
||||
|
||||
if ::ActiveRecord::VERSION::STRING >= "3.1"
|
||||
its(:alias_tracker) { should be_a ::ActiveRecord::Associations::AliasTracker }
|
||||
if AR_version >= "3.1"
|
||||
its(:alias_tracker) {
|
||||
should be_a ::ActiveRecord::Associations::AliasTracker
|
||||
}
|
||||
end
|
||||
|
||||
describe '#relation_for' do
|
||||
|
@ -22,7 +27,8 @@ module Ransack
|
|||
result = subject.evaluate(search)
|
||||
|
||||
expect(result).to be_an ::ActiveRecord::Relation
|
||||
expect(result.to_sql).to match /#{quote_column_name("name")} = 'Joe Blow'/
|
||||
expect(result.to_sql)
|
||||
.to match /#{quote_column_name("name")} = 'Joe Blow'/
|
||||
end
|
||||
|
||||
it 'SELECTs DISTINCT when distinct: true' do
|
||||
|
@ -38,12 +44,15 @@ module Ransack
|
|||
let(:shared_context) { Context.for(Person) }
|
||||
|
||||
before do
|
||||
Search.new(Person, {:parent_name_eq => 'A'}, context: shared_context)
|
||||
Search.new(Person, {:children_name_eq => 'B'}, context: shared_context)
|
||||
Search.new(Person, { :parent_name_eq => 'A' },
|
||||
context: shared_context)
|
||||
Search.new(Person, { :children_name_eq => 'B' },
|
||||
context: shared_context)
|
||||
end
|
||||
|
||||
describe '#join_associations', :if => ::ActiveRecord::VERSION::STRING <= '4.0' do
|
||||
it 'returns dependent join associations for all searches run against the context' do
|
||||
describe '#join_associations', :if => AR_version <= '4.0' do
|
||||
it 'returns dependent join associations for all searches run
|
||||
against the context' do
|
||||
parents, children = shared_context.join_associations
|
||||
|
||||
expect(children.aliased_table_name).to eq "children_people"
|
||||
|
@ -53,22 +62,28 @@ module Ransack
|
|||
it 'can be rejoined to execute a valid query' do
|
||||
parents, children = shared_context.join_associations
|
||||
|
||||
expect { Person.joins(parents).joins(children).to_a }.to_not raise_error
|
||||
expect { Person.joins(parents).joins(children).to_a }
|
||||
.to_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '#join_sources', :if => ::ActiveRecord::VERSION::STRING >= '3.1' do
|
||||
it 'returns dependent arel join nodes for all searches run against the context' do
|
||||
describe '#join_sources' do
|
||||
# FIXME: fix this test for Rails 4.2.
|
||||
it 'returns dependent arel join nodes for all searches run against
|
||||
the context',
|
||||
:if => %w(3.1 3.2 4.0 4.1).include?(AR_version) do
|
||||
parents, children = shared_context.join_sources
|
||||
|
||||
expect(children.left.name).to eq "children_people"
|
||||
expect(parents.left.name).to eq "parents_people"
|
||||
end
|
||||
|
||||
it 'can be rejoined to execute a valid query' do
|
||||
it 'can be rejoined to execute a valid query',
|
||||
:if => AR_version >= '3.1' do
|
||||
parents, children = shared_context.join_sources
|
||||
|
||||
expect { Person.joins(parents).joins(children).to_a }.to_not raise_error
|
||||
expect { Person.joins(parents).joins(children).to_a }
|
||||
.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,8 +51,8 @@ module Ransack
|
|||
end
|
||||
|
||||
describe '#sort_link with default search_key defined as symbol' do
|
||||
subject { @controller.
|
||||
view_context.sort_link(
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
Person.search(
|
||||
{ :sorts => ['name desc'] }, :search_key => :people_search
|
||||
),
|
||||
|
@ -71,6 +71,46 @@ module Ransack
|
|||
}
|
||||
end
|
||||
|
||||
describe '#sort_link desc through association table defined as a symbol' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
Person.search({ :sorts => ['comments_body asc'] }),
|
||||
:comments_body, :controller => 'people'
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./
|
||||
/people\?q%5Bs%5D=comments.body\+desc/
|
||||
else
|
||||
/people\?q(%5B|\[)s(%5D|\])=comments.body\+desc/
|
||||
end
|
||||
)
|
||||
}
|
||||
it { should match /sort_link asc/ }
|
||||
it { should match /Body ▲/ }
|
||||
end
|
||||
|
||||
describe '#sort_link through association table defined as a string' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
Person.search({ :sorts => ['comments.body desc'] }),
|
||||
'comments.body', :controller => 'people'
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./
|
||||
/people\?q%5Bs%5D=comments.body\+asc/
|
||||
else
|
||||
/people\?q(%5B|\[)s(%5D|\])=comments.body\+asc/
|
||||
end
|
||||
)
|
||||
}
|
||||
it { should match /sort_link desc/ }
|
||||
it { should match /Comments.body ▼/ }
|
||||
end
|
||||
|
||||
describe '#sort_link works even if search params are a blank string' do
|
||||
before { @controller.view_context.params[:q] = '' }
|
||||
specify {
|
||||
|
@ -105,6 +145,127 @@ module Ransack
|
|||
}
|
||||
end
|
||||
|
||||
describe '#sort_link with multiple search_keys defined as an array' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
[:main_app, Person.search(:sorts => ['name desc', 'email asc'])],
|
||||
:name, [:name, 'email DESC'],
|
||||
:controller => 'people'
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match /sort_link desc/
|
||||
}
|
||||
it {
|
||||
should match /Full Name ▼/
|
||||
}
|
||||
end
|
||||
|
||||
describe '#sort_link with multiple search_keys should allow a label to be specified' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
[:main_app, Person.search(:sorts => ['name desc', 'email asc'])],
|
||||
:name, [:name, 'email DESC'],
|
||||
'Property Name',
|
||||
:controller => 'people'
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match /Property Name ▼/
|
||||
}
|
||||
end
|
||||
|
||||
describe '#sort_link with multiple search_keys should flip multiple fields specified without a direction' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
[:main_app, Person.search(:sorts => ['name desc', 'email asc'])],
|
||||
:name, [:name, :email],
|
||||
:controller => 'people'
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match /sort_link desc/
|
||||
}
|
||||
it {
|
||||
should match /Full Name ▼/
|
||||
}
|
||||
end
|
||||
|
||||
describe '#sort_link with multiple search_keys should allow a default_order to be specified' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
[:main_app, Person.search()],
|
||||
:name, [:name, :email],
|
||||
:controller => 'people',
|
||||
:default_order => 'desc'
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match /sort_link/
|
||||
}
|
||||
it {
|
||||
should match /Full Name/
|
||||
}
|
||||
end
|
||||
|
||||
describe '#sort_link with multiple search_keys should allow multiple default_orders to be specified' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
[:main_app, Person.search()],
|
||||
:name, [:name, :email],
|
||||
:controller => 'people',
|
||||
:default_order => { 'name' => 'desc', :email => 'asc' }
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+asc/
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match /sort_link/
|
||||
}
|
||||
it {
|
||||
should match /Full Name/
|
||||
}
|
||||
end
|
||||
|
||||
describe '#sort_link with multiple search_keys with multiple default_orders should not override a specified order' do
|
||||
subject { @controller.view_context
|
||||
.sort_link(
|
||||
[:main_app, Person.search()],
|
||||
:name, [:name, 'email desc'],
|
||||
:controller => 'people',
|
||||
:default_order => { 'name' => 'desc', :email => 'asc' }
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match(
|
||||
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
|
||||
)
|
||||
}
|
||||
it {
|
||||
should match /sort_link/
|
||||
}
|
||||
it {
|
||||
should match /Full Name/
|
||||
}
|
||||
end
|
||||
context 'view has existing parameters' do
|
||||
before do
|
||||
@controller.view_context.params.merge!({ :exist => 'existing' })
|
||||
|
|
|
@ -135,7 +135,7 @@ module Schema
|
|||
t.string :only_admin
|
||||
t.integer :salary
|
||||
t.boolean :awesome, default: false
|
||||
t.timestamps
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :articles, :force => true do |t|
|
||||
|
|
Loading…
Reference in a new issue