diff --git a/.travis.yml b/.travis.yml
index b9246b22b..9c6a60425 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,18 +5,21 @@ language: ruby
sudo: false
rvm:
- - 2.2.1
+ - 2.3.0
+ - 2.2.4
- 2.1
- 2.0
- 1.9
env:
- - RAILS=4-2-stable DB=mongodb
+ - RAILS=4-2-stable DB=mongoid4
+ - RAILS=4-2-stable DB=mongoid5
- RAILS=4-2-stable DB=sqlite3
- RAILS=4-2-stable DB=mysql
- RAILS=4-2-stable DB=postgres
- - RAILS=4-1-stable DB=mongodb
+ - RAILS=4-1-stable DB=mongoid4
+ - RAILS=4-1-stable DB=mongoid5
- RAILS=4-1-stable DB=sqlite3
- RAILS=4-1-stable DB=mysql
- RAILS=4-1-stable DB=postgres
@@ -35,31 +38,40 @@ env:
matrix:
include:
- - rvm: 2.2
+ - rvm: 2.3.0
+ env: RAILS=master DB=mongoid5
+ - rvm: 2.3.0
+ env: RAILS=master DB=mongoid4
+ - rvm: 2.3.0
env: RAILS=master DB=sqlite3
- - rvm: 2.2
+ - rvm: 2.3.0
env: RAILS=master DB=mysql
- - rvm: 2.2
+ - rvm: 2.3.0
env: RAILS=master DB=postgres
- exclude:
- - rvm: 2.2
- env: RAILS=3-1-stable DB=sqlite
- - rvm: 2.2
- env: RAILS=3-1-stable DB=mysql
- - rvm: 2.2
- env: RAILS=3-1-stable DB=postgres
+
+ - rvm: 2.2.4
+ env: RAILS=master DB=mongoid5
+ - rvm: 2.2.4
+ env: RAILS=master DB=mongoid4
+ - rvm: 2.2.4
+ env: RAILS=master DB=sqlite3
+ - rvm: 2.2.4
+ env: RAILS=master DB=mysql
+ - rvm: 2.2.4
+ env: RAILS=master DB=postgres
+
allow_failures:
+ - env: RAILS=master DB=mongoid5
+ - env: RAILS=master DB=mongoid4
- env: RAILS=master DB=sqlite3
- env: RAILS=master DB=mysql
- env: RAILS=master DB=postgres
- - rvm: 2.2
- env: RAILS=3-2-stable DB=sqlite
- - rvm: 2.2
- env: RAILS=3-2-stable DB=mysql
- - rvm: 2.2
- env: RAILS=3-2-stable DB=postgres
before_script:
- mysql -e 'create database ransack collate utf8_general_ci;'
- mysql -e 'use ransack;show variables like "%character%";show variables like "%collation%";'
- psql -c 'create database ransack;' -U postgres
+
+addons:
+ code_climate:
+ repo_token: 8b701c4364d51a0217105e08c06922d600cec3d9e60d546a89e3ddfe46e0664e
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ae2694f8..b6c2087a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,210 @@
# Change Log
-## Version 1.6.6 - 2015-04-05
+## Unreleased
+### Added
+
+* Support Mongoid 5. PR [#636](https://github.com/activerecord-hackery/ransack/pull/636), commit
+ [9e5faf4](https://github.com/activerecord-hackery/ransack/commit/9e5faf4).
+
+ *Josef Šimánek*
+
+* Added optional block argument for the `sort_link` method. PR
+ [#604](https://github.com/activerecord-hackery/ransack/pull/604), commit
+ [997b856](https://github.com/activerecord-hackery/ransack/commit/997b856).
+
+ *Andrea Dal Ponte*
+
+* Added `ransack_alias` to allow users to customize the names for long
+ ransack field names. PR
+ [#623](https://github.com/activerecord-hackery/ransack/pull/623), commit
+ [e712ff1](https://github.com/activerecord-hackery/ransack/commit/e712ff1).
+
+ *Ray Zane*
+
+* Added support for searching on attributes that have been added to
+ Active Record models with `alias_attribute` (Rails >= 4 only). PR
+ [#592](https://github.com/activerecord-hackery/ransack/pull/592), commit
+ [549342a](https://github.com/activerecord-hackery/ransack/commit/549342a).
+
+ *Marten Schilstra*
+
+* Add ability to globally hide sort link order indicator arrows with
+ `Ransack.configure#hide_sort_order_indicators = true`. PR
+ [#577](https://github.com/activerecord-hackery/ransack/pull/577), commit
+ [95d4591](https://github.com/activerecord-hackery/ransack/commit/95d4591).
+
+ *Josh Hunter*, *Jon Atack*
+
+* Add failing tests to facilitate work on issue
+ [#566](https://github.com/activerecord-hackery/ransack/issues/566)
+ of passing boolean values to search scopes. PR
+ [#575](https://github.com/activerecord-hackery/ransack/pull/575).
+
+ *Marcel Eeken*
+
+* Add Brazilian Portuguese i18n locale file (`pt-BR.yml`). PR
+ [#581](https://github.com/activerecord-hackery/ransack/pull/581).
+
+ *Diego Henrique Domingues*
+
+* Add Indonesian (Bahasa) i18n locale file (`id.yml`). PR
+ [#612](https://github.com/activerecord-hackery/ransack/pull/612).
+
+ *Adam Pahlevi Baihaqi*
+
+* Add Japanese i18n locale file (`ja.yml`). PR
+ [#622](https://github.com/activerecord-hackery/ransack/pull/622).
+
+ *Masanobu Mizutani*
+
+### Fixed
+
+* Fix using aliased attributes in association searches, and add a failing
+ spec. PR [#602](https://github.com/activerecord-hackery/ransack/pull/602).
+
+ *Marten Schilstra*
+
+* Replace Active Record `table_exists?` API that was deprecated
+ [here](https://github.com/rails/rails/commit/152b85f) in Rails 5. Commit
+ [c9d2297](https://github.com/activerecord-hackery/ransack/commit/c9d2297).
+
+ *Jon Atack*
+
+* Adapt to changes in Rails 5 where AC::Parameters composes a HWIA instead of
+ inheriting from Hash starting from Rails commit rails/rails@14a3bd5. Commit
+ [ceafc05](https://github.com/activerecord-hackery/ransack/commit/ceafc05).
+
+ *Jon Atack*
+
+* Fix test `#sort_link with hide order indicator set to true` to fail properly
+ ([4f65b09](https://github.com/activerecord-hackery/ransack/commit/4f65b09)).
+ This spec, added in
+ [#473](https://github.com/activerecord-hackery/ransack/pull/473), tested
+ the presence of the attribute name instead of the absence of the order
+ indicators and did not fail when it should.
+
+ *Josh Hunter*, *Jon Atack*
+
+* Revert
+ [f858dd6](https://github.com/activerecord-hackery/ransack/commit/f858dd6).
+ Fixes [#553](https://github.com/activerecord-hackery/ransack/issues/553)
+ performance regression with the SQL Server adapter.
+
+ *sschwing3*
+
+* Fix invalid Chinese I18n locale file name by replacing "zh" with "zh-CN".
+ PR [#590](https://github.com/activerecord-hackery/ransack/pull/590).
+
+ *Ethan Yang*
+
### Changed
-* Upgrade Polyamorous dependency to version 1.2.0, which uses Module#prepend instead of monkey-patching for hooking into Active Record (with Ruby 2.x).
+* Memory/speed perf improvement: Freeze strings in array global constants and
+ move from using global string constants to frozen strings
+ ([381a83c](https://github.com/activerecord-hackery/ransack/commit/381a83c)
+ and
+ [ce114ec](https://github.com/activerecord-hackery/ransack/commit/ce114ec)).
+
+ *Jon Atack*
+
+* Escape underscore `_` wildcard characters with PostgreSQL and MySQL. PR
+ [#584](https://github.com/activerecord-hackery/ransack/issues/584).
+
+ *Igor Dobryn*
+
+
+## Version 1.7.0 - 2015-08-20
+### Added
+
+* Add Mongoid support for referenced/embedded relations. PR
+ [#498](https://github.com/activerecord-hackery/ransack/pull/498).
+ TODO: Missing spec coverage! Add documentation!
+
+ *Penn Su*
+
+* Add German i18n locale file (`de.yml`). PR
+ [#537](https://github.com/activerecord-hackery/ransack/pull/537).
+
+ *Philipp Weissensteiner*
+
+### Fixed
+
+* Fix
+ [#499](https://github.com/activerecord-hackery/ransack/issues/499) and
+ [#549](https://github.com/activerecord-hackery/ransack/issues/549).
+ Ransack now loads only Active Record if both Active Record and Mongoid are
+ running to avoid the two adapters overriding each other. This clarifies
+ that Ransack currently knows how to work with only one database adapter
+ active at a time. PR
+ [#541](https://github.com/activerecord-hackery/ransack/pull/541).
+
+ *ASnow (Большов Андрей)*
+
+* Fix [#299](https://github.com/activerecord-hackery/ransack/issues/299)
+ `attribute_method?` parsing for attribute names containing `_and_`
+ and `_or_`. Attributes named like `foo_and_bar` or `foo_or_bar` are
+ recognized now instead of running failing checks for `foo` and `bar`.
+ PR [#562](https://github.com/activerecord-hackery/ransack/pull/562).
+
+ *Ryohei Hoshi*
+
+* Fix a time-dependent test failure. When the database has
+ `default_timezone = :local` (system time) and the `Time.zone` is set to
+ elsewhere, then `Date.current` does not match what the query produces for
+ the stored timestamps. Resolved by setting everything to UTC. PR
+ [#561](https://github.com/activerecord-hackery/ransack/pull/561).
+
+ *Andrew Vit*
+
+* Avoid overwriting association conditions with default scope in Rails 3.
+ When a model with default scope was associated with conditions
+ (`has_many :x, conditions: ...`), the default scope would overwrite the
+ association conditions. This patch ensures that both sources of conditions
+ are applied. 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 this issue was fixed since Rails 4. PR
+ [#560](https://github.com/activerecord-hackery/ransack/pull/560).
+
+ *Andrew Vit*
+
+* Fix RSpec `its` method deprecation warning: "Use of rspec-core's its
+ method is deprecated. Use the rspec-its gem instead"
+ ([c09aa17](https://github.com/activerecord-hackery/ransack/commit/c09aa17)).
+
+* Fix deprecated RSpec syntax in `grouping_spec.rb`
+ ([ba92a0b](https://github.com/activerecord-hackery/ransack/commit/ba92a0b)).
+
+ *Jon Atack*
+
+### Changed
+
+* Upgrade gemspec dependencies: MySQL2 from '0.3.14' to '0.3.18', and RSpec
+ from '~> 2.14.0' to '~> 2' which loads 2.99
+ ([000cd22](https://github.com/activerecord-hackery/ransack/commit/000cd22)).
+
+* Upgrade spec suite to RSpec 3 `expect` syntax backward compatible with
+ RSpec 2.9
+ ([87cd36d](https://github.com/activerecord-hackery/ransack/commit/87cd36d)
+ and
+ [d296caa](https://github.com/activerecord-hackery/ransack/commit/d296caa)).
+
+* Various FormHelper refactorings
+ ([17dd97a](https://github.com/activerecord-hackery/ransack/commit/17dd97a)
+ and
+ [29a73b9](https://github.com/activerecord-hackery/ransack/commit/29a73b9)).
+
+* Various documentation updates.
+
+ *Jon Atack*
+
+
+## Version 1.6.6 - 2015-04-05
+### Added
+
+* Add the Ruby version to the the header message that shows the database,
+ Active Record and Arel versions when running tests.
+
+* Add Code Climate analysis.
*Jon Atack*
@@ -28,12 +229,10 @@
*Jon Atack*
-### Added
-
-* Add the Ruby version to the the header message that shows the database,
- Active Record and Arel versions when running tests.
+### Changed
-* Add Code Climate analysis.
+* Upgrade Polyamorous dependency to version 1.2.0, which uses `Module#prepend`
+ instead of `alias_method` for hooking into Active Record (with Ruby 2.x).
*Jon Atack*
@@ -150,7 +349,7 @@
*Josh Kovach*
-* Add an sort_link option to not display sort direction arrows
+* Add an sort_link option to not display sort order indicator arrows
([PR #473](https://github.com/activerecord-hackery/ransack/pull/473)).
*Fred Bergman*
@@ -338,7 +537,7 @@
*Pedro Chambino*
-* Add `ro.yml` Romanian translation file.
+* Add Romanian i18n locale file (`ro.yml`).
*Andreas Philippi*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b57e1b628..dac873fc6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,16 +1,20 @@
# Contributing to Ransack
-Please take a moment to review this document in order to make the contribution
-process easy and effective for everyone involved!
+Please take a moment to review this document to make contributing easy and
+effective for everyone involved!
Ransack is an open source project and we encourage contributions.
+Please do not use the issue tracker for personal support requests. Stack
+Overflow is a better place for that where a wider community can help you!
+
## Filing an issue
-A bug is a _demonstrable problem_ that is caused by the code in the repository.
-Good bug reports are extremely helpful! Please do not use the issue tracker for personal support requests.
+Good issue reports are extremely helpful! Please only open an issue if a bug
+is caused by Ransack, is new (has not already been reported), and can be
+reproduced from the information you provide.
-Guidelines for bug reports:
+Steps:
1. **Use the GitHub issue search** — check if the issue has already been
reported.
@@ -18,18 +22,25 @@ Guidelines for bug reports:
2. **Check if the issue has been fixed** — try to reproduce it using the
`master` branch in the repository.
-3. **Isolate and report the problem** — ideally create a reduced test
- case.
+3. **Isolate the real problem** — make sure the issue is really a bug in
+ Ransack and not in your code or another gem.
+
+4. **Report the issue** by providing the link to a self-contained
+ gist like [this](https://gist.github.com/jonatack/63048bc5062a84ba9e09) or
+ [this](https://gist.github.com/jonatack/5df41a0edb53b7bad989). Please use
+ these code examples as a bug-report template for your Ransack issue!
-When filing an issue, please provide these details:
+If you do not provide a self-contained gist and would like your issue to be reviewed, do provide at a minimum:
-* A comprehensive list of steps to reproduce the issue, or - far better - **a failing spec**.
-* The version (and branch) of Ransack *and* the versions of Rails, Ruby, and your operating system.
+* A comprehensive list of steps to reproduce the issue, or even better, a
+ passing/failing test spec.
+* Whether you are using Ransack through another gem like ActiveAdmin,
+ SimpleForm, etc.
+* The versions of Ruby, Rails, Ransack and the database.
* Any relevant stack traces ("Full trace" preferred).
-Any issue that is open for 14 days without actionable information or activity
-will be marked as "stalled" and then closed. Stalled issues can be re-opened
-if the information requested is provided.
+Issues filed without the above information or that remain open without activity
+for 14 days will be closed. They can be re-opened if actionable information to reproduce the issue is provided.
## Pull requests
@@ -76,20 +87,24 @@ Here's a quick guide:
8. Update the Change Log. If you are adding new functionality, document it in
the README.
-9. Commit your changes (`git commit -am 'Add feature/fix bug/improve docs'`).
+9. Make sure git knows your name and email address in your `~/.gitconfig` file:
+
+ $ git config --global user.name "Your Name"
+ $ git config --global user.email "contributor@example.com"
-10. If necessary, rebase your commits into logical chunks, without errors. To
+10. Commit your changes (`git commit -am 'Add feature/fix bug/improve docs'`).
+ If your pull request only contains documentation changes, please remember
+ to add `[skip ci]` to the beginning of your commit message so the Travis
+ test suite doesn't :runner: needlessly.
+
+11. If necessary, rebase your commits into logical chunks, without errors. To
interactively rebase and cherry-pick from, say, the last 10 commits:
`git rebase -i HEAD~10`, then `git push -f`.
-11. Push the branch up to your fork on Github
- (`git push origin my-new-feature`) and from Github submit a pull request to
+12. Push the branch up to your fork on GitHub
+ (`git push origin my-new-feature`) and from GitHub submit a pull request to
Ransack's `master` branch.
-12. If your pull request only contains documentation changes, please remember
- to add `[skip ci]` to the beginning of your commit message so the Travis
- test suite doesn't :runner: needlessly.
-
At this point you're waiting on us. We like to at least comment on, if not
accept, pull requests within three business days (and, typically, one business
day). We may suggest some changes or improvements or alternatives.
@@ -107,11 +122,11 @@ Syntax:
* 80 characters per line.
* No trailing whitespace. Blank lines should not have any space.
* Prefer `&&`/`||` over `and`/`or`.
-* `MyClass.my_method(my_arg)` not `my_method( my_arg )` or my_method my_arg.
+* `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
* `a = b` and not `a=b`.
* `a_method { |block| ... }` and not `a_method { | block | ... }` or
`a_method{|block| ...}`.
* Prefer simplicity, readability, and maintainability over terseness.
* Follow the conventions you see used in the code already.
-And in case we didn't emphasize it enough: we love tests!
+And in case we didn't emphasize it enough: We love tests!
diff --git a/Gemfile b/Gemfile
index 63897fa64..2d050f722 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,10 +6,11 @@ gem 'rake'
rails = ENV['RAILS'] || '4-2-stable'
if rails == 'master'
+ gem 'rack', github: 'rack/rack'
gem 'arel', github: 'rails/arel'
gem 'polyamorous', github: 'activerecord-hackery/polyamorous'
else
- gem 'polyamorous', '~> 1.2'
+ gem 'polyamorous', '~> 1.3'
end
gem 'pry'
@@ -41,10 +42,14 @@ else
end
end
-if ENV['DB'] =~ /mongodb/
+if ENV['DB'] =~ /mongoid4/
gem 'mongoid', '~> 4.0.0', require: false
end
+if ENV['DB'] =~ /mongoid5/
+ gem 'mongoid', '~> 5.0.0', require: false
+end
+
# Removed from Ruby 2.2 but needed for testing Rails 3.x.
group :test do
gem 'test-unit', '~> 3.0' if RUBY_VERSION >= '2.2'
diff --git a/README.md b/README.md
index 4f4667466..5b08f07e0 100644
--- a/README.md
+++ b/README.md
@@ -29,18 +29,19 @@ instead.
If you're viewing this at
[github.com/activerecord-hackery/ransack](https://github.com/activerecord-hackery/ransack),
you're reading the documentation for the master branch with the latest features.
-[View documentation for the last release (1.6.6).]
-(https://github.com/activerecord-hackery/ransack/tree/v1.6.6)
+[View documentation for the last release (1.7.0).](https://github.com/activerecord-hackery/ransack/tree/v1.7.0)
## Getting started
-Ransack is compatible with Rails 3 and 4 (including 4.2.1) on Ruby 1.9 and
-later (Ruby 2.2 recommended). Ransack currently works with Rails master (5.0.0)
-too! If you are using Ruby 1.8, you can use an earlier version of Ransack up to
-1.3.0.
+Ransack is compatible with Rails 3, 4 and 5 on Ruby 1.9 and later.
+JRuby 9 ought to work as well (see
+[this](https://github.com/activerecord-hackery/polyamorous/issues/17)).
+If you are using Ruby 1.8 or an earlier JRuby and run into compatibility
+issues, you can use an earlier version of Ransack, say, up to 1.3.0.
-Ransack works out-of-the-box with Active Record and also features experimental
-support for Mongoid 4.0 (without associations, further details below).
+Ransack works out-of-the-box with Active Record and also features limited
+support for Mongoid 4 and 5 (without associations, further details
+[below](https://github.com/activerecord-hackery/ransack#mongoid)).
In your Gemfile, for the last officially released gem:
@@ -54,6 +55,19 @@ Or, if you would like to use the latest updates, use the `master` branch:
gem 'ransack', github: 'activerecord-hackery/ransack'
```
+September 2015 update: If you are using Rails 5 (master) and need pagination
+that works with Ransack, there is an
+[updated version of the `will_paginate` gem here](https://github.com/jonatack/will_paginate).
+It is also optimized for Ruby 2.2+. To use it, in your Gemfile:
+`gem 'will_paginate', github: 'jonatack/will_paginate'`.
+
+## Issues tracker
+
+* Before filing an issue, please read the [Contributing Guide](CONTRIBUTING.md).
+* File an issue if a bug is caused by Ransack, is new (has not already been reported), and _can be reproduced from the information you provide_.
+* Contributions are welcome, but please do not add "+1" comments to issues or pull requests :smiley:
+* Please do not use the issue tracker for personal support requests. Stack Overflow is a better place for that where a wider community can help you!
+
## Usage
Ransack can be used in one of two modes, simple or advanced.
@@ -79,22 +93,6 @@ If you're coming from MetaSearch, things to note:
ActiveRecord::Relation in the case of the ActiveRecord adapter) via a call to
`Ransack#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. It generates the same SQL as calling `uniq` on the relation.
-
- 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.
-
- If `distinct: true` or `uniq` is causing invalid SQL, another way to remove
- duplicates is to call `to_a.uniq` on the collection at the end (see the next
- section below) -- with the caveat that the de-duping is taking place in Ruby
- instead of in SQL, which is potentially slower and uses more memory, and that
- it may display awkwardly with pagination if the number of results is greater
- than the page size.
-
####In your controller
```ruby
@@ -103,7 +101,7 @@ def index
@people = @q.result(distinct: true)
end
```
-or without `distinct:true`, for sorting on an associated table's columns (in
+or without `distinct: true`, for sorting on an associated table's columns (in
this example, with preloading each Person's Articles and pagination):
```ruby
@@ -170,6 +168,14 @@ column title or a default sort order:
<%= sort_link(@q, :name, 'Last Name', default_order: :desc) %>
```
+You can use a block if the link markup is hard to fit into the label parameter:
+
+```erb
+<%= sort_link(@q, :name) do %>
+ Player Name
+<% end %>
+```
+
With a polymorphic association, you may need to specify the name of the link
explicitly to avoid an `uninitialized constant Model::Xxxable` error (see issue
[#421](https://github.com/activerecord-hackery/ransack/issues/421)):
@@ -206,6 +212,15 @@ The sort link may be displayed without the order indicator arrow by passing
<%= sort_link(@q, :name, hide_indicator: true) %>
```
+Alternatively, all sort links may be displayed without the order indicator arrow
+by adding this to an initializer file like `config/initializers/ransack.rb`:
+
+```ruby
+Ransack.configure do |c|
+ c.hide_sort_order_indicators = true
+end
+```
+
### Advanced Mode
"Advanced" searches (ab)use Rails' nested attributes functionality in order to
@@ -257,7 +272,7 @@ Article.search(params[:q])
```
Users have reported issues of `#search` name conflicts with other gems, so
-the `#search` method alias might be deprecated in the next major version of
+the `#search` method alias will be deprecated in the next major version of
Ransack (2.0). It's advisable to use the default `#ransack` instead.
For now, if Ransack's `#search` method conflicts with the name of another
@@ -328,15 +343,40 @@ end
...
<%= content_tag :table do %>
<%= content_tag :th, sort_link(@q, :last_name) %>
- <%= content_tag :th, sort_link(@q, 'departments.title') %>
- <%= content_tag :th, sort_link(@q, 'employees.last_name') %>
+ <%= content_tag :th, sort_link(@q, :department_title) %>
+ <%= content_tag :th, sort_link(@q, :employees_last_name) %>
<% end %>
```
-Please note that in a sort link, the association is expressed as an SQL string
-(`'employees.last_name'`) with a pluralized table name, instead of the symbol
-`:employee_last_name` syntax with a class#underscore table name used for
-Ransack objects elsewhere.
+If you have trouble sorting on associations, try using an SQL string with the
+pluralized table (`'departments.title'`,`'employees.last_name'`) instead of the
+symbolized association (`:department_title)`, `:employees_last_name`).
+
+### Ransack Aliases
+
+You can customize the attribute names for your Ransack searches by using a
+`ransack_alias`. This is particularly useful for long attribute names that are
+necessary when querying associations or multiple columns.
+
+```ruby
+class Post < ActiveRecord::Base
+ belongs_to :author
+
+ # Abbreviate :author_first_name_or_author_last_name to :author
+ ransack_alias :author, :author_first_name_or_author_last_name
+end
+```
+
+Now, rather than using `:author_first_name_or_author_last_name_cont` in your
+form, you can simply use `:author_cont`. This serves to produce more expressive
+query parameters in your URLs.
+
+```erb
+<%= search_form_for @q do |f| %>
+ <%= f.label :author_cont %>
+ <%= f.search_field :author_cont %>
+<% end %>
+```
### Using Ransackers to add custom search functions via Arel
@@ -347,6 +387,58 @@ information about `ransacker` methods can be found [here in the wiki]
(https://github.com/activerecord-hackery/ransack/wiki/Using-Ransackers).
Feel free to contribute working `ransacker` code examples to the wiki!
+### Problem with DISTINCT selects
+
+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. It generates the same SQL as calling `uniq` on the relation.
+
+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 will
+will need to modify the result as needed to allow these queries to work.
+
+For example, you could call joins and includes on the result which has the
+effect of adding those tables columns to the select statement, overcoming
+the issue, like so:
+
+```ruby
+def index
+ @q = Person.ransack(params[:q])
+ @people = @q.result(distinct: true)
+ .includes(:articles)
+ .joins(:articles)
+ .page(params[:page])
+end
+```
+
+If the above doesn't help, you can also use ActiveRecord's `select` query
+to explicitly add the columns you need, which brute force's adding the
+columns you need that your SQL engine is complaining about, you need to
+make sure you give all of the columns you care about, for example:
+
+```ruby
+def index
+ @q = Person.ransack(params[:q])
+ @people = @q.result(distinct: true)
+ .select('people.*, articles.name, articles.description')
+ .page(params[:page])
+end
+```
+
+A final way of last resort is to call `to_a.uniq` on the collection at the end
+with the caveat that the de-duping is taking place in Ruby instead of in SQL,
+which is potentially slower and uses more memory, and that it may display
+awkwardly with pagination if the number of results is greater than the page size.
+
+For example:
+
+```ruby
+def index
+ @q = Person.ransack(params[:q])
+ @people = @q.result.includes(:articles).page(params[:page]).to_a.uniq
+end
+```
+
### Authorization (whitelisting/blacklisting)
By default, searching and sorting are authorized on any column of your model
@@ -476,7 +568,7 @@ scope accepts a value:
```ruby
class Employee < ActiveRecord::Base
- scope :active, ->(boolean = true) { where(active: boolean) }
+ scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
@@ -490,24 +582,23 @@ class Employee < ActiveRecord::Base
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
- %i(active hired_since salary_gt)
+ %i(activated hired_since salary_gt)
else
- # allow other users to search on active and hired_since only
- %i(active hired_since)
+ # allow other users to search on `activated` and `hired_since` only
+ %i(activated hired_since)
end
end
end
-Employee.ransack({ active: true, hired_since: '2013-01-01' })
+Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
```
-If the `true` value is being passed via url params or by some other mechanism
-that will convert it to a string (i.e. `active: 'true'` instead of
-`active: true`), the true value will *not* be passed to the scope. If you want
-to pass a `'true'` string to the scope, you should wrap it in an array (i.e.
-`active: ['true']`).
+In Rails 3 and 4, if the `true` value is being passed via url params or some
+other mechanism that will convert it to a string, the true value may not be
+passed to the ransackable scope unless you wrap it in an array
+(i.e. `activated: ['true']`). This is currently resolved in Rails 5 :smiley:
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
@@ -655,6 +746,17 @@ called on a `ransack` search returns a `Mongoid::Criteria` object:
@people = @q.result.active.order_by(updated_at: -1).limit(10)
```
+_NOTE: Ransack currently works with either Active Record or Mongoid, but not
+both in the same application. If both are present, Ransack will default to
+Active Record only. Here is the code containing the logic:_
+
+```ruby
+ @current_adapters ||= {
+ :active_record => defined?(::ActiveRecord::Base),
+ :mongoid => defined?(::Mongoid) && !defined?(::ActiveRecord::Base)
+ }
+```
+
## Semantic Versioning
Ransack attempts to follow semantic versioning in the format of `x.y.z`, where:
@@ -681,7 +783,3 @@ directly related to bug reports, pull requests, or documentation improvements.
* Spread the word on Twitter, Facebook, and elsewhere if Ransack's been useful
to you. The more people who are using the project, the quicker we can find and
fix bugs!
-
-## Copyright
-
-Copyright © 2011-2015 [Ernie Miller](http://twitter.com/erniemiller)
diff --git a/Rakefile b/Rakefile
index 67cbb1892..215d944a4 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,15 +1,14 @@
require 'bundler'
require 'rspec/core/rake_task'
-require 'active_record'
Bundler::GemHelper.install_tasks
RSpec::Core::RakeTask.new(:spec) do |rspec|
ENV['SPEC'] = 'spec/ransack/**/*_spec.rb'
- if ActiveRecord::VERSION::MAJOR >= 4 || RUBY_VERSION < '2.2'
- # Raises `invalid option: --backtrace` with Rails 3.x on Ruby 2.2
- rspec.rspec_opts = ['--backtrace']
- end
+ # With Rails 3, using `--backtrace` raises 'invalid option' when testing.
+ # With Rails 4 and 5 it can be uncommented to see the backtrace:
+ #
+ # rspec.rspec_opts = ['--backtrace']
end
RSpec::Core::RakeTask.new(:mongoid) do |rspec|
@@ -18,7 +17,7 @@ RSpec::Core::RakeTask.new(:mongoid) do |rspec|
end
task :default do
- if ENV['DB'] =~ /mongodb/
+ if ENV['DB'] =~ /mongoid/
Rake::Task["mongoid"].invoke
else
Rake::Task["spec"].invoke
diff --git a/lib/ransack.rb b/lib/ransack.rb
index 136ea3007..f33732313 100644
--- a/lib/ransack.rb
+++ b/lib/ransack.rb
@@ -2,15 +2,19 @@
require 'ransack/configuration'
-if defined?(::Mongoid)
- require 'ransack/adapters/mongoid/ransack/constants'
-else
- require 'ransack/adapters/active_record/ransack/constants'
-end
+require 'ransack/adapters'
+Ransack::Adapters.require_constants
module Ransack
extend Configuration
class UntraversableAssociationError < StandardError; end;
+
+ SUPPORTS_ATTRIBUTE_ALIAS =
+ begin
+ ActiveRecord::Base.respond_to?(:attribute_aliases)
+ rescue NameError
+ false
+ end
end
Ransack.configure do |config|
@@ -29,14 +33,6 @@ class UntraversableAssociationError < StandardError; end;
require 'ransack/translate'
-if defined?(::ActiveRecord::Base)
- require 'ransack/adapters/active_record/ransack/translate'
- require 'ransack/adapters/active_record'
-end
-
-if defined?(::Mongoid)
- require 'ransack/adapters/mongoid/ransack/translate'
- require 'ransack/adapters/mongoid'
-end
+Ransack::Adapters.require_adapter
ActionController::Base.helper Ransack::Helpers::FormHelper
diff --git a/lib/ransack/adapters.rb b/lib/ransack/adapters.rb
new file mode 100644
index 000000000..affc0b1f1
--- /dev/null
+++ b/lib/ransack/adapters.rb
@@ -0,0 +1,42 @@
+module Ransack
+ module Adapters
+
+ def self.current_adapters
+ @current_adapters ||= {
+ :active_record => defined?(::ActiveRecord::Base),
+ :mongoid => defined?(::Mongoid) && !defined?(::ActiveRecord::Base)
+ }
+ end
+ def self.require_constants
+ require 'ransack/adapters/mongoid/ransack/constants' if current_adapters[:mongoid]
+ require 'ransack/adapters/active_record/ransack/constants' if current_adapters[:active_record]
+ end
+
+ def self.require_adapter
+ if current_adapters[:active_record]
+ require 'ransack/adapters/active_record/ransack/translate'
+ require 'ransack/adapters/active_record'
+ end
+
+ if current_adapters[:mongoid]
+ require 'ransack/adapters/mongoid/ransack/translate'
+ require 'ransack/adapters/mongoid'
+ end
+ end
+
+ def self.require_context
+ require 'ransack/adapters/active_record/ransack/visitor' if current_adapters[:active_record]
+ require 'ransack/adapters/mongoid/ransack/visitor' if current_adapters[:mongoid]
+ end
+
+ def self.require_nodes
+ require 'ransack/adapters/active_record/ransack/nodes/condition' if current_adapters[:active_record]
+ require 'ransack/adapters/mongoid/ransack/nodes/condition' if current_adapters[:mongoid]
+ end
+
+ def self.require_search
+ require 'ransack/adapters/active_record/ransack/context' if current_adapters[:active_record]
+ require 'ransack/adapters/mongoid/ransack/context' if current_adapters[:mongoid]
+ end
+ end
+end
diff --git a/lib/ransack/adapters/active_record/3.0/compat.rb b/lib/ransack/adapters/active_record/3.0/compat.rb
index c42e08e95..acbe21085 100644
--- a/lib/ransack/adapters/active_record/3.0/compat.rb
+++ b/lib/ransack/adapters/active_record/3.0/compat.rb
@@ -138,16 +138,16 @@ def visit_Arel_Nodes_NamedFunction o
"#{
o.name
}(#{
- o.distinct ? Ransack::Constants::DISTINCT : Ransack::Constants::EMPTY
+ o.distinct ? Ransack::Constants::DISTINCT : ''.freeze
}#{
- o.expressions.map { |x| visit x }.join(Ransack::Constants::COMMA_SPACE)
+ o.expressions.map { |x| visit x }.join(', '.freeze)
})#{
- o.alias ? " AS #{visit o.alias}" : Ransack::Constants::EMPTY
+ o.alias ? " AS #{visit o.alias}" : ''.freeze
}"
end
def visit_Arel_Nodes_And o
- o.children.map { |x| visit x }.join(Ransack::Constants::SPACED_AND)
+ o.children.map { |x| visit x }.join(' AND '.freeze)
end
def visit_Arel_Nodes_Not o
@@ -164,7 +164,7 @@ def visit_Arel_Nodes_Values o
quote(value, attr && column_for(attr))
end
}
- .join(Ransack::Constants::COMMA_SPACE)
+ .join(', '.freeze)
})"
end
end
diff --git a/lib/ransack/adapters/active_record/3.0/context.rb b/lib/ransack/adapters/active_record/3.0/context.rb
index 60367a29f..f9ad0f726 100644
--- a/lib/ransack/adapters/active_record/3.0/context.rb
+++ b/lib/ransack/adapters/active_record/3.0/context.rb
@@ -124,7 +124,7 @@ def get_association(str, parent = @base)
end
def join_dependency(relation)
- if relation.respond_to?(:join_dependency) # Squeel will enable this
+ if relation.respond_to?(:join_dependency) # Polyamorous enables this
relation.join_dependency
else
build_join_dependency(relation)
diff --git a/lib/ransack/adapters/active_record/3.1/context.rb b/lib/ransack/adapters/active_record/3.1/context.rb
index 7be77ef76..2a718d2e8 100644
--- a/lib/ransack/adapters/active_record/3.1/context.rb
+++ b/lib/ransack/adapters/active_record/3.1/context.rb
@@ -137,7 +137,7 @@ def get_association(str, parent = @base)
end
def join_dependency(relation)
- if relation.respond_to?(:join_dependency) # Squeel will enable this
+ if relation.respond_to?(:join_dependency) # Polyamorous enables this
relation.join_dependency
else
build_join_dependency(relation)
diff --git a/lib/ransack/adapters/active_record/base.rb b/lib/ransack/adapters/active_record/base.rb
index 66b2b8e55..c4ca93543 100644
--- a/lib/ransack/adapters/active_record/base.rb
+++ b/lib/ransack/adapters/active_record/base.rb
@@ -7,7 +7,9 @@ def self.extended(base)
alias :search :ransack unless base.respond_to? :search
base.class_eval do
class_attribute :_ransackers
+ class_attribute :_ransack_aliases
self._ransackers ||= {}
+ self._ransack_aliases ||= {}
end
end
@@ -20,12 +22,21 @@ def ransacker(name, opts = {}, &block)
.new(self, name, opts, &block)
end
+ def ransack_alias(new_name, old_name)
+ self._ransack_aliases.store(new_name.to_s, old_name.to_s)
+ 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
+ if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
+ column_names + _ransackers.keys + _ransack_aliases.keys +
+ attribute_aliases.keys
+ else
+ column_names + _ransackers.keys + _ransack_aliases.keys
+ end
end
# Ransackable_associations, by default, returns the names
diff --git a/lib/ransack/adapters/active_record/context.rb b/lib/ransack/adapters/active_record/context.rb
index 86ff423a2..4f68d6502 100644
--- a/lib/ransack/adapters/active_record/context.rb
+++ b/lib/ransack/adapters/active_record/context.rb
@@ -22,13 +22,13 @@ def relation_for(object)
def type_for(attr)
return nil unless attr && attr.valid?
- name = attr.arel_attribute.name.to_s
- table = attr.arel_attribute.relation.table_name
- connection = attr.klass.connection
- unless connection.table_exists?(table)
- raise "No table named #{table} exists"
+ name = attr.arel_attribute.name.to_s
+ table = attr.arel_attribute.relation.table_name
+ schema_cache = @engine.connection.schema_cache
+ unless schema_cache.send(database_table_exists?, table)
+ raise "No table named #{table} exists."
end
- connection.schema_cache.columns_hash(table)[name].type
+ schema_cache.columns_hash(table)[name].type
end
def evaluate(search, opts = {})
@@ -86,7 +86,7 @@ def join_associations
"ActiveRecord 4.1 and later does not use join_associations. Use join_sources."
end
- # All dependent Arel::Join nodes used in the search query
+ # All dependent Arel::Join nodes used in the search query.
#
# This could otherwise be done as `@object.arel.join_sources`, except
# that ActiveRecord's build_joins sets up its own JoinDependency.
@@ -94,13 +94,18 @@ def join_associations
# JoinDependency to track table aliases.
#
def join_sources
- base =
- if ::ActiveRecord::VERSION::MAJOR >= 5
- Arel::SelectManager.new(@object.table)
- else
- Arel::SelectManager.new(@object.engine, @object.table)
- end
- joins = @join_dependency.join_constraints(@object.joins_values)
+ base, joins =
+ if ::ActiveRecord::VERSION::MAJOR >= 5
+ [
+ Arel::SelectManager.new(@object.table),
+ @join_dependency.join_constraints(@object.joins_values, @join_type)
+ ]
+ else
+ [
+ Arel::SelectManager.new(@object.engine, @object.table),
+ @join_dependency.join_constraints(@object.joins_values)
+ ]
+ end
joins.each do |aliased_join|
base.from(aliased_join)
end
@@ -109,7 +114,7 @@ def join_sources
else
- # All dependent JoinAssociation items used in the search query
+ # All dependent JoinAssociation items used in the search query.
#
# Deprecated: this goes away in ActiveRecord 4.1. Use join_sources.
#
@@ -134,6 +139,14 @@ def alias_tracker
private
+ def database_table_exists?
+ if ::ActiveRecord::VERSION::MAJOR >= 5
+ :data_source_exists?
+ else
+ :table_exists?
+ end
+ end
+
def get_parent_and_attribute_name(str, parent = @base)
attr_name = nil
@@ -168,7 +181,7 @@ def get_association(str, parent = @base)
end
def join_dependency(relation)
- if relation.respond_to?(:join_dependency) # Squeel will enable this
+ if relation.respond_to?(:join_dependency) # Polyamorous enables this
relation.join_dependency
else
build_joins(relation)
@@ -282,7 +295,7 @@ def build_or_find_association(name, parent = @base, klass = nil)
:build,
Polyamorous::Join.new(name, @join_type, klass),
parent
- )
+ )
found_association = @join_dependency.join_associations.last
# Leverage the stashed association functionality in AR
@object = @object.joins(found_association)
diff --git a/lib/ransack/adapters/active_record/ransack/constants.rb b/lib/ransack/adapters/active_record/ransack/constants.rb
index c02ee2075..0844106db 100644
--- a/lib/ransack/adapters/active_record/ransack/constants.rb
+++ b/lib/ransack/adapters/active_record/ransack/constants.rb
@@ -4,97 +4,97 @@ module Constants
DERIVED_PREDICATES = [
[CONT, {
- :arel_predicate => 'matches'.freeze,
- :formatter => proc { |v| "%#{escape_wildcards(v)}%" }
+ arel_predicate: 'matches'.freeze,
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
}
],
['not_cont'.freeze, {
- :arel_predicate => 'does_not_match'.freeze,
- :formatter => proc { |v| "%#{escape_wildcards(v)}%" }
+ arel_predicate: 'does_not_match'.freeze,
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
}
],
['start'.freeze, {
- :arel_predicate => 'matches'.freeze,
- :formatter => proc { |v| "#{escape_wildcards(v)}%" }
+ arel_predicate: 'matches'.freeze,
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
}
],
['not_start'.freeze, {
- :arel_predicate => 'does_not_match'.freeze,
- :formatter => proc { |v| "#{escape_wildcards(v)}%" }
+ arel_predicate: 'does_not_match'.freeze,
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
}
],
['end'.freeze, {
- :arel_predicate => 'matches'.freeze,
- :formatter => proc { |v| "%#{escape_wildcards(v)}" }
+ arel_predicate: 'matches'.freeze,
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
}
],
['not_end'.freeze, {
- :arel_predicate => 'does_not_match'.freeze,
- :formatter => proc { |v| "%#{escape_wildcards(v)}" }
+ arel_predicate: 'does_not_match'.freeze,
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
}
],
['true'.freeze, {
- :arel_predicate => proc { |v| v ? EQ : NOT_EQ },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| true }
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| true }
}
],
['not_true'.freeze, {
- :arel_predicate => proc { |v| v ? NOT_EQ : EQ },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| true }
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| true }
}
],
['false'.freeze, {
- :arel_predicate => proc { |v| v ? EQ : NOT_EQ },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| false }
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| false }
}
],
['not_false'.freeze, {
- :arel_predicate => proc { |v| v ? NOT_EQ : EQ },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| false }
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| false }
}
],
['present'.freeze, {
- :arel_predicate => proc { |v| v ? NOT_EQ_ALL : EQ_ANY },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| [nil, EMPTY] }
+ arel_predicate: proc { |v| v ? NOT_EQ_ALL : EQ_ANY },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| [nil, ''.freeze].freeze }
}
],
['blank'.freeze, {
- :arel_predicate => proc { |v| v ? EQ_ANY : NOT_EQ_ALL },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| [nil, EMPTY] }
+ arel_predicate: proc { |v| v ? EQ_ANY : NOT_EQ_ALL },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| [nil, ''.freeze].freeze }
}
],
['null'.freeze, {
- :arel_predicate => proc { |v| v ? EQ : NOT_EQ },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v)},
- :formatter => proc { |v| nil }
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v)},
+ formatter: proc { |v| nil }
}
],
['not_null'.freeze, {
- :arel_predicate => proc { |v| v ? NOT_EQ : EQ },
- :compounds => false,
- :type => :boolean,
- :validator => proc { |v| BOOLEAN_VALUES.include?(v) },
- :formatter => proc { |v| nil } }
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
+ compounds: false,
+ type: :boolean,
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
+ formatter: proc { |v| nil } }
]
].freeze
@@ -104,7 +104,7 @@ def escape_wildcards(unescaped)
case ActiveRecord::Base.connection.adapter_name
when "Mysql2".freeze, "PostgreSQL".freeze
# Necessary for PostgreSQL and MySQL
- unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
+ unescaped.to_s.gsub(/([\\|\%|_|.])/, '\\\\\\1')
else
unescaped
end
diff --git a/lib/ransack/adapters/mongoid/base.rb b/lib/ransack/adapters/mongoid/base.rb
index 740b3c876..a952a7c79 100644
--- a/lib/ransack/adapters/mongoid/base.rb
+++ b/lib/ransack/adapters/mongoid/base.rb
@@ -33,6 +33,14 @@ def quote_column_name name
end
module ClassMethods
+ def _ransack_aliases
+ @_ransack_aliases ||= {}
+ end
+
+ def _ransack_aliases=(value)
+ @_ransack_aliases = value
+ end
+
def _ransackers
@_ransackers ||= {}
end
@@ -49,13 +57,18 @@ def ransack(params = {}, options = {})
alias_method :search, :ransack
+ def ransack_alias(new_name, old_name)
+ self._ransack_aliases.store(new_name.to_s, old_name.to_s)
+ end
+
def ransacker(name, opts = {}, &block)
self._ransackers = _ransackers.merge name.to_s => Ransacker
.new(self, name, opts, &block)
end
def all_ransackable_attributes
- ['id'] + column_names.select { |c| c != '_id' } + _ransackers.keys
+ ['id'] + column_names.select { |c| c != '_id' } + _ransackers.keys +
+ _ransack_aliases.keys
end
def ransackable_attributes(auth_object = nil)
@@ -73,7 +86,9 @@ def ransackable_associations(auth_object = nil)
end
def reflect_on_all_associations_all
- reflect_on_all_associations(:belongs_to, :has_one, :has_many)
+ reflect_on_all_associations(
+ :belongs_to, :has_one, :has_many, :embeds_many, :embedded_in
+ )
end
# For overriding with a whitelist of symbols
@@ -87,6 +102,10 @@ def joins_values *args
[]
end
+ def custom_join_ast *args
+ []
+ end
+
def first(*args)
if args.size == 0
super
@@ -112,10 +131,10 @@ def columns_hash
end
def table
- name = ::Ransack::Adapters::Mongoid::Attributes::Attribute.new(self.criteria, :name)
- {
- :name => name
- }
+ name = ::Ransack::Adapters::Mongoid::Attributes::Attribute.new(
+ self.criteria, :name
+ )
+ { :name => name }
end
end
diff --git a/lib/ransack/adapters/mongoid/context.rb b/lib/ransack/adapters/mongoid/context.rb
index 07a687f74..1847f5d84 100644
--- a/lib/ransack/adapters/mongoid/context.rb
+++ b/lib/ransack/adapters/mongoid/context.rb
@@ -21,7 +21,7 @@ def relation_for(object)
def type_for(attr)
return nil unless attr && attr.valid?
- name = attr.arel_attribute.name.to_s
+ name = attr.arel_attribute.name.to_s.split('.').last
# table = attr.arel_attribute.relation.table_name
# schema_cache = @engine.connection.schema_cache
@@ -38,7 +38,7 @@ def type_for(attr)
name = '_id' if name == 'id'
- t = object.klass.fields[name].type
+ t = object.klass.fields[name].try(:type) || @bind_pairs[attr.name].first.fields[name].type
t.to_s.demodulize.underscore.to_sym
end
@@ -114,10 +114,10 @@ def get_parent_and_attribute_name(str, parent = @base)
segments.pop) && segments.size > 0 && !found_assoc do
assoc, klass = unpolymorphize_association(segments.join('_'))
if found_assoc = get_association(assoc, parent)
- join = build_or_find_association(found_assoc.name, parent, klass)
parent, attr_name = get_parent_and_attribute_name(
- remainder.join('_'), join
+ remainder.join('_'), found_assoc.klass
)
+ attr_name = "#{segments.join('_')}.#{attr_name}"
end
end
end
@@ -132,7 +132,7 @@ def get_association(str, parent = @base)
end
def join_dependency(relation)
- if relation.respond_to?(:join_dependency) # Squeel will enable this
+ if relation.respond_to?(:join_dependency) # Polyamorous enables this
relation.join_dependency
else
build_join_dependency(relation)
diff --git a/lib/ransack/configuration.rb b/lib/ransack/configuration.rb
index 9568c83ea..21fdcf8d0 100644
--- a/lib/ransack/configuration.rb
+++ b/lib/ransack/configuration.rb
@@ -8,7 +8,8 @@ module Configuration
self.predicates = {}
self.options = {
:search_key => :q,
- :ignore_unknown_conditions => true
+ :ignore_unknown_conditions => true,
+ :hide_sort_order_indicators => false
}
def configure
@@ -61,12 +62,32 @@ def search_key=(name)
self.options[:search_key] = name
end
- # Raise an error if an unknown predicate, condition or attribute is passed
- # into a search.
+ # By default Ransack ignores errors if an unknown predicate, condition or
+ # attribute is passed into a search. The default may be overridden in an
+ # initializer file like `config/initializers/ransack.rb` as follows:
+ #
+ # Ransack.configure do |config|
+ # # Raise if an unknown predicate, condition or attribute is passed
+ # config.ignore_unknown_conditions = false
+ # end
+ #
def ignore_unknown_conditions=(boolean)
self.options[:ignore_unknown_conditions] = boolean
end
+ # By default, Ransack displays sort order indicator arrows in sort links.
+ # The default may be globally overridden in an initializer file like
+ # `config/initializers/ransack.rb` as follows:
+ #
+ # Ransack.configure do |config|
+ # # Hide sort link order indicators globally across the application
+ # config.hide_sort_order_indicators = true
+ # end
+ #
+ def hide_sort_order_indicators=(boolean)
+ self.options[:hide_sort_order_indicators] = boolean
+ end
+
def arel_predicate_with_suffix(arel_predicate, suffix)
if arel_predicate === Proc
proc { |v| "#{arel_predicate.call(v)}#{suffix}" }
diff --git a/lib/ransack/constants.rb b/lib/ransack/constants.rb
index 656d3d708..b2f02c961 100644
--- a/lib/ransack/constants.rb
+++ b/lib/ransack/constants.rb
@@ -1,19 +1,10 @@
module Ransack
module Constants
- ASC = 'asc'.freeze
- DESC = 'desc'.freeze
- ASC_DESC = [ASC, DESC].freeze
-
ASC_ARROW = '▲'.freeze
DESC_ARROW = '▼'.freeze
OR = 'or'.freeze
AND = 'and'.freeze
- SPACED_AND = ' AND '.freeze
-
- SORT = 'sort'.freeze
- SORT_LINK = 'sort_link'.freeze
- SORT_DIRECTION = 'sort_direction'.freeze
CAP_SEARCH = 'Search'.freeze
SEARCH = 'search'.freeze
@@ -23,17 +14,12 @@ module Constants
ATTRIBUTES = 'attributes'.freeze
COMBINATOR = 'combinator'.freeze
- SPACE = ' '.freeze
- COMMA_SPACE = ', '.freeze
- COLON_SPACE = ': '.freeze
TWO_COLONS = '::'.freeze
UNDERSCORE = '_'.freeze
LEFT_PARENTHESIS = '('.freeze
Q = 'q'.freeze
I = 'i'.freeze
- NON_BREAKING_SPACE = ' '.freeze
DOT_ASTERIX = '.*'.freeze
- EMPTY = ''.freeze
STRING_JOIN = 'string_join'.freeze
ASSOCIATION_JOIN = 'association_join'.freeze
@@ -44,14 +30,17 @@ module Constants
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
BOOLEAN_VALUES = (TRUE_VALUES + FALSE_VALUES).freeze
- S_SORTS = %w(s sorts).freeze
- AND_OR = %w(and or).freeze
- IN_NOT_IN = %w(in not_in).freeze
- SUFFIXES = %w(_any _all).freeze
- AREL_PREDICATES = %w(
- eq not_eq matches does_not_match lt lteq gt gteq in not_in
- ).freeze
- A_S_I = %w(a s i).freeze
+ AND_OR = ['and'.freeze, 'or'.freeze].freeze
+ IN_NOT_IN = ['in'.freeze, 'not_in'.freeze].freeze
+ SUFFIXES = ['_any'.freeze, '_all'.freeze].freeze
+ AREL_PREDICATES = [
+ 'eq'.freeze, 'not_eq'.freeze,
+ 'matches'.freeze, 'does_not_match'.freeze,
+ 'lt'.freeze, 'lteq'.freeze,
+ 'gt'.freeze, 'gteq'.freeze,
+ 'in'.freeze, 'not_in'.freeze
+ ].freeze
+ A_S_I = ['a'.freeze, 's'.freeze, 'i'.freeze].freeze
EQ = 'eq'.freeze
NOT_EQ = 'not_eq'.freeze
diff --git a/lib/ransack/context.rb b/lib/ransack/context.rb
index e1295e57e..a5474d56b 100644
--- a/lib/ransack/context.rb
+++ b/lib/ransack/context.rb
@@ -1,12 +1,5 @@
require 'ransack/visitor'
-
-if defined?(::ActiveRecord::Base)
- require 'ransack/adapters/active_record/ransack/visitor'
-end
-
-if defined?(::Mongoid)
- require 'ransack/adapters/mongoid/ransack/visitor'
-end
+Ransack::Adapters.require_context
module Ransack
class Context
@@ -24,9 +17,12 @@ def for_object(object, options = {})
end
def for(object, options = {})
- context = Class === object ?
- for_class(object, options) :
- for_object(object, options)
+ context =
+ if Class === object
+ for_class(object, options)
+ else
+ for_object(object, options)
+ end
context or raise ArgumentError,
"Don't know what context to use for #{object}"
end
@@ -66,7 +62,7 @@ def bind(object, str)
end
def traverse(str, base = @base)
- str ||= Constants::EMPTY
+ str ||= ''.freeze
if (segments = str.split(/_/)).size > 0
remainder = []
@@ -75,13 +71,12 @@ def traverse(str, base = @base)
# Strip the _of_Model_type text from the association name, but hold
# onto it in klass, for use as the next base
assoc, klass = unpolymorphize_association(
- segments.join(Constants::UNDERSCORE)
+ segments.join('_'.freeze)
)
if found_assoc = get_association(assoc, base)
base = traverse(
- remainder.join(
- Constants::UNDERSCORE), klass || found_assoc.klass
- )
+ remainder.join('_'.freeze), klass || found_assoc.klass
+ )
end
remainder.unshift segments.pop
@@ -95,7 +90,7 @@ def traverse(str, base = @base)
def association_path(str, base = @base)
base = klassify(base)
- str ||= Constants::EMPTY
+ str ||= ''.freeze
path = []
segments = str.split(/_/)
association_parts = []
@@ -125,6 +120,10 @@ def unpolymorphize_association(str)
end
end
+ def ransackable_alias(str)
+ klass._ransack_aliases.fetch(str, str)
+ end
+
def ransackable_attribute?(str, klass)
klass.ransackable_attributes(auth_object).include?(str) ||
klass.ransortable_attributes(auth_object).include?(str)
@@ -138,15 +137,15 @@ def ransackable_scope?(str, klass)
klass.ransackable_scopes(auth_object).any? { |s| s.to_s == str }
end
- def searchable_attributes(str = Constants::EMPTY)
+ def searchable_attributes(str = ''.freeze)
traverse(str).ransackable_attributes(auth_object)
end
- def sortable_attributes(str = Constants::EMPTY)
+ def sortable_attributes(str = ''.freeze)
traverse(str).ransortable_attributes(auth_object)
end
- def searchable_associations(str = Constants::EMPTY)
+ def searchable_associations(str = ''.freeze)
traverse(str).ransackable_associations(auth_object)
end
diff --git a/lib/ransack/helpers/form_builder.rb b/lib/ransack/helpers/form_builder.rb
index 4e84dab35..61d0f4d39 100644
--- a/lib/ransack/helpers/form_builder.rb
+++ b/lib/ransack/helpers/form_builder.rb
@@ -15,8 +15,7 @@ def value(object)
RANSACK_FORM_BUILDER = 'RANSACK_FORM_BUILDER'.freeze
require 'simple_form' if
- (ENV[RANSACK_FORM_BUILDER] || Ransack::Constants::EMPTY)
- .match('SimpleForm'.freeze)
+ (ENV[RANSACK_FORM_BUILDER] || ''.freeze).match('SimpleForm'.freeze)
module Ransack
module Helpers
@@ -47,7 +46,7 @@ def attribute_select(options = nil, html_options = nil, action = nil)
raise ArgumentError, formbuilder_error_message(
"#{action}_select") unless object.respond_to?(:context)
options[:include_blank] = true unless options.has_key?(:include_blank)
- bases = [Constants::EMPTY] + association_array(options[:associations])
+ bases = [''.freeze].freeze + association_array(options[:associations])
if bases.size > 1
collection = attribute_collection_for_bases(action, bases)
object.name ||= default if can_use_default?(
@@ -66,13 +65,13 @@ def attribute_select(options = nil, html_options = nil, action = nil)
def sort_direction_select(options = {}, html_options = {})
unless object.respond_to?(:context)
raise ArgumentError,
- formbuilder_error_message(Constants::SORT_DIRECTION)
+ formbuilder_error_message('sort_direction'.freeze)
end
template_collection_select(:dir, sort_array, options, html_options)
end
def sort_select(options = {}, html_options = {})
- attribute_select(options, html_options, Constants::SORT) +
+ attribute_select(options, html_options, 'sort'.freeze) +
sort_direction_select(options, html_options)
end
@@ -135,7 +134,7 @@ def predicate_select(options = {}, html_options = {})
else
only = Array.wrap(only).map(&:to_s)
keys = keys.select {
- |k| only.include? k.sub(/_(any|all)$/, Constants::EMPTY)
+ |k| only.include? k.sub(/_(any|all)$/, ''.freeze)
}
end
end
diff --git a/lib/ransack/helpers/form_helper.rb b/lib/ransack/helpers/form_helper.rb
index cdf09f891..aa2e2a0dd 100644
--- a/lib/ransack/helpers/form_helper.rb
+++ b/lib/ransack/helpers/form_helper.rb
@@ -15,8 +15,7 @@ def search_form_for(record, options = {}, &proc)
elsif record.is_a?(Array) &&
(search = record.detect { |o| o.is_a?(Ransack::Search) })
options[:url] ||= polymorphic_path(
- record.map { |o| o.is_a?(Ransack::Search) ? o.klass : o },
- format: options.delete(:format)
+ options_for(record), format: options.delete(:format)
)
else
raise ArgumentError,
@@ -24,13 +23,9 @@ def search_form_for(record, options = {}, &proc)
end
options[:html] ||= {}
html_options = {
- :class => options[:class].present? ?
- "#{options[:class]}" :
- "#{search.klass.to_s.underscore}_search",
- :id => options[:id].present? ?
- "#{options[:id]}" :
- "#{search.klass.to_s.underscore}_search",
- :method => :get
+ class: html_option_for(options[:class], search),
+ id: html_option_for(options[:id], search),
+ method: :get
}
options[:as] ||= Ransack.options[:search_key]
options[:html].reverse_merge!(html_options)
@@ -43,23 +38,41 @@ def search_form_for(record, options = {}, &proc)
#
# <%= sort_link(@q, :name, [:name, 'kind ASC'], 'Player Name') %>
#
- def sort_link(search_object, attribute, *args)
+ # You can also use a block:
+ #
+ # <%= sort_link(@q, :name, [:name, 'kind ASC']) do %>
+ # Player Name
+ # <% end %>
+ #
+ def sort_link(search_object, attribute, *args, &block)
search, routing_proxy = extract_search_and_routing_proxy(search_object)
unless Search === search
raise TypeError, 'First argument must be a Ransack::Search!'
end
- s = SortLink.new(search, attribute, args, params)
+ args.unshift(capture(&block)) if block_given?
+ s = SortLink.new(search, attribute, args, params, &block)
link_to(s.name, url(routing_proxy, s.url_options), s.html_options(args))
end
private
+ def options_for(record)
+ record.map { |r| parse_record(r) }
+ end
+
+ def parse_record(object)
+ return object.klass if object.is_a?(Ransack::Search)
+ object
+ end
+
+ def html_option_for(option, search)
+ return option.to_s if option.present?
+ "#{search.klass.to_s.underscore}_search"
+ end
+
def extract_search_and_routing_proxy(search)
- if search.is_a? Array
- [search.second, search.first]
- else
- [search, nil]
- end
+ return [search[1], search[0]] if search.is_a?(Array)
+ [search, nil]
end
def url(routing_proxy, options_for_url)
@@ -73,22 +86,21 @@ def url(routing_proxy, options_for_url)
class SortLink
def initialize(search, attribute, args, params)
@search = search
- @params = params
+ @params = parameters_hash(params)
@field = attribute.to_s
- sort_fields = extract_sort_fields_and_mutate_args!(args).compact
+ @sort_fields = extract_sort_fields_and_mutate_args!(args).compact
@current_dir = existing_sort_direction
@label_text = extract_label_and_mutate_args!(args)
@options = extract_options_and_mutate_args!(args)
- @hide_indicator = @options.delete :hide_indicator
+ @hide_indicator = @options.delete(:hide_indicator) ||
+ Ransack.options[:hide_sort_order_indicators]
@default_order = @options.delete :default_order
- @sort_params = build_sort(sort_fields)
- @sort_params = @sort_params.first if @sort_params.size == 1
end
def name
[ERB::Util.h(@label_text), order_indicator]
.compact
- .join(Constants::NON_BREAKING_SPACE)
+ .join(' '.freeze)
.html_safe
end
@@ -100,49 +112,51 @@ def url_options
def html_options(args)
html_options = extract_options_and_mutate_args!(args)
- html_options.merge(class:
- [[Constants::SORT_LINK, @current_dir], html_options[:class]]
- .compact.join(Constants::SPACE)
- )
+ html_options.merge(
+ class: [['sort_link'.freeze, @current_dir], html_options[:class]]
+ .compact.join(' '.freeze)
+ )
end
private
+ def parameters_hash(params)
+ return params unless params.respond_to?(:to_unsafe_h)
+ params.to_unsafe_h
+ end
+
def extract_sort_fields_and_mutate_args!(args)
- if args.first.is_a? Array
- args.shift
- else
- [@field]
- end
+ return args.shift if args[0].is_a?(Array)
+ [@field]
end
def extract_label_and_mutate_args!(args)
- if args.first.is_a? String
- args.shift
- else
- Translate.attribute(@field, :context => @search.context)
- end
+ return args.shift if args[0].is_a?(String)
+ Translate.attribute(@field, context: @search.context)
end
def extract_options_and_mutate_args!(args)
- if args.first.is_a? Hash
- args.shift.with_indifferent_access
- else
- {}
- end
+ return args.shift.with_indifferent_access if args[0].is_a?(Hash)
+ {}
end
def search_and_sort_params
- search_params.merge(:s => @sort_params)
+ search_params.merge(s: sort_params)
end
def search_params
@params[@search.context.search_key].presence || {}
end
- def build_sort(fields)
+ def sort_params
+ sort_array = recursive_sort_params_build(@sort_fields)
+ return sort_array[0] if sort_array.length == 1
+ sort_array
+ end
+
+ def recursive_sort_params_build(fields)
return [] if fields.empty?
- [parse_sort(fields[0])] + build_sort(fields.drop(1))
+ [parse_sort(fields[0])] + recursive_sort_params_build(fields.drop 1)
end
def parse_sort(field)
@@ -154,52 +168,41 @@ def parse_sort(field)
end
def detect_previous_sort_direction_and_invert_it(attr_name)
- sort_dir = existing_sort_direction(attr_name)
- if sort_dir
+ if sort_dir = existing_sort_direction(attr_name)
direction_text(sort_dir)
else
- default_sort_order(attr_name) || Constants::ASC
+ default_sort_order(attr_name) || 'asc'.freeze
end
end
- def existing_sort_direction(attr_name = @field)
- if sort = @search.sorts.detect { |s| s && s.name == attr_name }
- sort.dir
- end
+ def existing_sort_direction(f = @field)
+ return unless sort = @search.sorts.detect { |s| s && s.name == f }
+ sort.dir
end
def default_sort_order(attr_name)
- Hash === @default_order ? @default_order[attr_name] : @default_order
+ return @default_order[attr_name] if Hash === @default_order
+ @default_order
end
def order_indicator
- if @hide_indicator || no_sort_direction_specified?
- nil
- else
- direction_arrow
- end
+ return if @hide_indicator || no_sort_direction_specified?
+ direction_arrow
end
def no_sort_direction_specified?(dir = @current_dir)
- !Constants::ASC_DESC.include?(dir)
+ !['asc'.freeze, 'desc'.freeze].freeze.include?(dir)
end
def direction_arrow
- if @current_dir == Constants::DESC
- Constants::DESC_ARROW
- else
- Constants::ASC_ARROW
- end
+ return Constants::DESC_ARROW if @current_dir == 'desc'.freeze
+ Constants::ASC_ARROW
end
def direction_text(dir)
- if dir == Constants::DESC
- Constants::ASC
- else
- Constants::DESC
- end
+ return 'asc'.freeze if dir == 'desc'.freeze
+ 'desc'.freeze
end
-
end
end
end
diff --git a/lib/ransack/locale/id.yml b/lib/ransack/locale/id.yml
new file mode 100644
index 000000000..20a93ccf5
--- /dev/null
+++ b/lib/ransack/locale/id.yml
@@ -0,0 +1,70 @@
+id:
+ ransack:
+ search: "cari"
+ predicate: "predikat"
+ and: "dan"
+ or: "atau"
+ any: "apapun"
+ all: "semua"
+ combinator: "kombinasi"
+ attribute: "atribut"
+ value: "data"
+ condition: "kondisi"
+ sort: "urutan"
+ asc: "ascending"
+ desc: "descending"
+ predicates:
+ eq: "sama dengan"
+ eq_any: "sama beberapa dengan"
+ eq_all: "sama seluruhnya dengan"
+ not_eq: "tidak sama dengan"
+ not_eq_any: "tidak sama beberapa dengan"
+ not_eq_all: "tidak semua seluruhnya dengan"
+ matches: "mirip"
+ matches_any: "mirip beberapa dengan"
+ matches_all: "mirip semua dengan"
+ does_not_match: "tidak mirip dengan"
+ does_not_match_any: "tidak mirip beberapa dengan"
+ does_not_match_all: "tidak mirip semua dengan"
+ lt: "kurang dari"
+ lt_any: "kurang beberapa dengan"
+ lt_all: "kurang seluruhnya dengan"
+ lteq: "kurang lebih"
+ lteq_any: "kurang lebih beberapa dengan"
+ lteq_all: "kurang lebih semua dengan"
+ gt: "lebih besar daripada"
+ gt_any: "lebih besar beberapa dengan"
+ gt_all: "lebih besar semua dengan"
+ gteq: "lebih besar atau sama dengan"
+ gteq_any: "beberapa lebih besar atau sama dengan"
+ gteq_all: "semua lebih besar atau sama dengan"
+ in: "di"
+ in_any: "di beberapa"
+ in_all: "di semua"
+ not_in: "tidak di"
+ not_in_any: "tidak di beberapa"
+ not_in_all: "tidak semua di"
+ cont: "mengandung"
+ cont_any: "mengandung beberapa"
+ cont_all: "mengandung semua"
+ not_cont: "tidak mengandung"
+ not_cont_any: "tidak mengandung beberapa"
+ not_cont_all: "tidak mengandung semua"
+ start: "diawali dengan"
+ start_any: "diawali beberapa dengan"
+ start_all: "diawali semua dengan"
+ not_start: "tidak diawali dengan"
+ not_start_any: "tidak diawali beberapa dengan"
+ not_start_all: "tidak diawali semua dengan"
+ end: "diakhiri dengan"
+ end_any: "diakhiri beberapa dengan"
+ end_all: "diakhiri semua dengan"
+ not_end: "tidak diakhiri dengan"
+ not_end_any: "tidak diakhiri dengan beberapa"
+ not_end_all: "tidak diakhiri dengan semua"
+ 'true': "bernilai benar"
+ 'false': "bernilai salah"
+ present: "ada"
+ blank: "kosong"
+ 'null': "null"
+ not_null: "tidak null"
diff --git a/lib/ransack/locale/ja.yml b/lib/ransack/locale/ja.yml
new file mode 100644
index 000000000..fc5c67c2f
--- /dev/null
+++ b/lib/ransack/locale/ja.yml
@@ -0,0 +1,70 @@
+ja:
+ ransack:
+ search: "検索"
+ predicate: "は以下である"
+ and: "と"
+ or: "あるいは"
+ any: "いずれか"
+ all: "全て"
+ combinator: "組み合わせ"
+ attribute: "属性"
+ value: "値"
+ condition: "状態"
+ sort: "分類"
+ asc: "昇順"
+ desc: "降順"
+ predicates:
+ eq: "は以下と等しい"
+ eq_any: "は以下のいずれかに等しい"
+ eq_all: "は以下の全てに等しい"
+ not_eq: "は以下と等しくない"
+ not_eq_any: "は以下のいずれかに等しくない"
+ not_eq_all: "は以下の全てと等しくない"
+ matches: "は以下と合致している"
+ matches_any: "は以下のいずれかと合致している"
+ matches_all: "は以下の全てと合致している"
+ does_not_match: "は以下と合致していない"
+ does_not_match_any: "は以下のいずれかに合致していない"
+ does_not_match_all: "は以下の全てに合致していない"
+ lt: "は以下よりも小さい"
+ lt_any: "は以下のいずれかより小さい"
+ lt_all: "は以下の全てよりも小さい"
+ lteq: "は以下より小さいか等しい"
+ lteq_any: "は以下のいずれかより小さいか等しい"
+ lteq_all: "は以下の全てより小さいか等しい"
+ gt: "は以下より大きい"
+ gt_any: "は以下のいずれかより大きい"
+ gt_all: "は以下の全てより大きい"
+ gteq: "は以下より大きいか等しい"
+ gteq_any: "は以下のいずれかより大きいか等しい"
+ gteq_all: "は以下の全てより大きいか等しい"
+ in: "は以下の範囲内である"
+ in_any: "は以下のいずれかの範囲内である"
+ in_all: "は以下の全ての範囲内である"
+ not_in: "は以下の範囲内でない"
+ not_in_any: "は以下のいずれかの範囲内でない"
+ not_in_all: "は以下の全ての範囲内"
+ cont: "は以下を含む"
+ cont_any: "はいずれかを含む"
+ cont_all: "は以下の全てを含む"
+ not_cont: "は含まない"
+ not_cont_any: "は以下のいずれかを含まない"
+ not_cont_all: "は以下の全てを含まない"
+ start: "は以下で始まる"
+ start_any: "は以下のどれかで始まる"
+ start_all: "は以下の全てで始まる"
+ not_start: "は以下で始まらない"
+ not_start_any: "は以下のいずれかで始まらない"
+ not_start_all: "は以下の全てで始まらない"
+ end: "は以下で終わる"
+ end_any: "は以下のいずれかで終わる"
+ end_all: "は以下の全てで終わる"
+ not_end: "は以下のどれでも終わらない"
+ not_end_any: "は以下のいずれかで終わらない"
+ not_end_all: "は以下の全てで終わらない"
+ 'true': "真"
+ 'false': "偽"
+ present: "は存在する"
+ blank: "は空である"
+ 'null': "無効"
+ not_null: "は無効ではない"
diff --git a/lib/ransack/locale/pt-BR.yml b/lib/ransack/locale/pt-BR.yml
new file mode 100644
index 000000000..9c059377f
--- /dev/null
+++ b/lib/ransack/locale/pt-BR.yml
@@ -0,0 +1,70 @@
+pt-BR:
+ ransack:
+ search: "pesquisar"
+ predicate: "predicado"
+ and: "e"
+ or: "ou"
+ any: "algum"
+ all: "todos"
+ combinator: "combinador"
+ attribute: "atributo"
+ value: "valor"
+ condition: "condição"
+ sort: "classificar"
+ asc: "ascendente"
+ desc: "descendente"
+ predicates:
+ eq: "igual"
+ eq_any: "igual a algum"
+ eq_all: "igual a todos"
+ not_eq: "não é igual a"
+ not_eq_any: "não é igual a algum"
+ not_eq_all: "não é igual a todos"
+ matches: "corresponde"
+ matches_any: "corresponde a algum"
+ matches_all: "corresponde a todos"
+ does_not_match: "não corresponde"
+ does_not_match_any: "não corresponde a algum"
+ does_not_match_all: "não corresponde a todos"
+ lt: "menor que"
+ lt_any: "menor que algum"
+ lt_all: "menor que todos"
+ lteq: "menor ou igual a"
+ lteq_any: "menor ou igual a algum"
+ lteq_all: "menor ou igual a todos"
+ gt: "maior que"
+ gt_any: "maior que algum"
+ gt_all: "maior que todos"
+ gteq: "maior que ou igual a"
+ gteq_any: "maior que ou igual a algum"
+ gteq_all: "maior que ou igual a todos"
+ in: "em"
+ in_any: "em algum"
+ in_all: "em todos"
+ not_in: "não em"
+ not_in_any: "não em algum"
+ not_in_all: "não em todos"
+ cont: "contém"
+ cont_any: "contém algum"
+ cont_all: "contém todos"
+ not_cont: "não contém"
+ not_cont_any: "não contém algum"
+ not_cont_all: "não contém todos"
+ start: "começa com"
+ start_any: "começa com algum"
+ start_all: "começa com todos"
+ not_start: "não começa com"
+ not_start_any: "não começa com algum"
+ not_start_all: "não começa com algum"
+ end: "termina com"
+ end_any: "termina com algum"
+ end_all: "termina com todos"
+ not_end: "não termina com"
+ not_end_any: "não termina com algum"
+ not_end_all: "não termina com todos"
+ 'true': "é verdadeiro"
+ 'false': "é falso"
+ present: "está presente"
+ blank: "está em branco"
+ 'null': "é nullo"
+ not_null: "não é nulo"
diff --git a/lib/ransack/locale/zh.yml b/lib/ransack/locale/zh-CN.yml
similarity index 99%
rename from lib/ransack/locale/zh.yml
rename to lib/ransack/locale/zh-CN.yml
index df6b5e085..b9a7e994b 100644
--- a/lib/ransack/locale/zh.yml
+++ b/lib/ransack/locale/zh-CN.yml
@@ -1,4 +1,4 @@
-zh:
+zh-CN:
ransack:
search: "搜索"
predicate: "基于(predicate)"
diff --git a/lib/ransack/nodes.rb b/lib/ransack/nodes.rb
index a8447cd92..63946a70e 100644
--- a/lib/ransack/nodes.rb
+++ b/lib/ransack/nodes.rb
@@ -3,7 +3,6 @@
require 'ransack/nodes/attribute'
require 'ransack/nodes/value'
require 'ransack/nodes/condition'
-require 'ransack/adapters/active_record/ransack/nodes/condition' if defined?(::ActiveRecord::Base)
-require 'ransack/adapters/mongoid/ransack/nodes/condition' if defined?(::Mongoid)
+Ransack::Adapters.require_nodes
require 'ransack/nodes/sort'
-require 'ransack/nodes/grouping'
\ No newline at end of file
+require 'ransack/nodes/grouping'
diff --git a/lib/ransack/nodes/attribute.rb b/lib/ransack/nodes/attribute.rb
index 69d84fd03..0b2fc3b1f 100644
--- a/lib/ransack/nodes/attribute.rb
+++ b/lib/ransack/nodes/attribute.rb
@@ -22,7 +22,7 @@ def name=(name)
def valid?
bound? && attr &&
context.klassify(parent).ransackable_attributes(context.auth_object)
- .include?(attr_name)
+ .include?(attr_name.split('.').last)
end
def type
diff --git a/lib/ransack/nodes/bindable.rb b/lib/ransack/nodes/bindable.rb
index b80d8aec0..4615df1d8 100644
--- a/lib/ransack/nodes/bindable.rb
+++ b/lib/ransack/nodes/bindable.rb
@@ -27,14 +27,26 @@ def reset_binding!
private
- def get_arel_attribute
- if ransacker
- ransacker.attr_from(self)
- else
- context.table_for(parent)[attr_name]
- end
+ def get_arel_attribute
+ if ransacker
+ ransacker.attr_from(self)
+ else
+ get_attribute
end
+ end
+ def get_attribute
+ if is_alias_attribute?
+ context.table_for(parent)[parent.base_klass.attribute_aliases[attr_name]]
+ else
+ context.table_for(parent)[attr_name]
+ end
+ end
+
+ def is_alias_attribute?
+ Ransack::SUPPORTS_ATTRIBUTE_ALIAS &&
+ parent.base_klass.attribute_aliases.key?(attr_name)
+ end
end
end
end
diff --git a/lib/ransack/nodes/condition.rb b/lib/ransack/nodes/condition.rb
index 385874135..5bd3982af 100644
--- a/lib/ransack/nodes/condition.rb
+++ b/lib/ransack/nodes/condition.rb
@@ -9,9 +9,10 @@ class Condition < Node
class << self
def extract(context, key, values)
- attributes, predicate = extract_attributes_and_predicate(key)
+ attributes, predicate, combinator =
+ extract_values_for_condition(key, context)
+
if attributes.size > 0 && predicate
- combinator = key.match(/_(or|and)_/) ? $1 : nil
condition = self.new(context)
condition.build(
:a => attributes,
@@ -31,16 +32,34 @@ def extract(context, key, values)
private
- def extract_attributes_and_predicate(key)
- str = key.dup
- name = Predicate.detect_and_strip_from_string!(str)
- predicate = Predicate.named(name)
- unless predicate || Ransack.options[:ignore_unknown_conditions]
- raise ArgumentError, "No valid predicate for #{key}"
+ def extract_values_for_condition(key, context = nil)
+ str = key.dup
+ name = Predicate.detect_and_strip_from_string!(str)
+ predicate = Predicate.named(name)
+
+ unless predicate || Ransack.options[:ignore_unknown_conditions]
+ raise ArgumentError, "No valid predicate for #{key}"
+ end
+
+ if context.present?
+ str = context.ransackable_alias(str)
+ end
+
+ combinator =
+ if str.match(/_(or|and)_/)
+ $1
+ else
+ nil
+ end
+
+ if context.present? && context.attribute_method?(str)
+ attributes = [str]
+ else
+ attributes = str.split(/_and_|_or_/)
+ end
+
+ [attributes, predicate, combinator]
end
- attributes = str.split(/_and_|_or_/)
- [attributes, predicate]
- end
end
def valid?
@@ -192,15 +211,20 @@ def formatted_values_for_attribute(attr)
val = predicate.format(val)
val
end
- predicate.wants_array ? formatted : formatted.first
+ if predicate.wants_array
+ formatted
+ else
+ formatted.first
+ end
end
def arel_predicate_for_attribute(attr)
if predicate.arel_predicate === Proc
values = casted_values_for_attribute(attr)
- predicate.arel_predicate.call(
- predicate.wants_array ? values : values.first
- )
+ unless predicate.wants_array
+ values = values.first
+ end
+ predicate.arel_predicate.call(values)
else
predicate.arel_predicate
end
@@ -220,7 +244,7 @@ def inspect
]
.reject { |e| e[1].blank? }
.map { |v| "#{v[0]}: #{v[1]}" }
- .join(Constants::COMMA_SPACE)
+ .join(', '.freeze)
"Condition <#{data}>"
end
diff --git a/lib/ransack/nodes/grouping.rb b/lib/ransack/nodes/grouping.rb
index 438c2f5f0..643ffd95e 100644
--- a/lib/ransack/nodes/grouping.rb
+++ b/lib/ransack/nodes/grouping.rb
@@ -68,7 +68,7 @@ def values
def respond_to?(method_id)
super or begin
method_name = method_id.to_s
- writer = method_name.sub!(/\=$/, Constants::EMPTY)
+ writer = method_name.sub!(/\=$/, ''.freeze)
attribute_method?(method_name) ? true : false
end
end
@@ -114,7 +114,7 @@ def groupings=(groupings)
def method_missing(method_id, *args)
method_name = method_id.to_s
- writer = method_name.sub!(/\=$/, Constants::EMPTY)
+ writer = method_name.sub!(/\=$/, ''.freeze)
if attribute_method?(method_name)
if writer
write_attribute(method_name, *args)
@@ -169,7 +169,7 @@ def inspect
]
.reject { |e| e[1].blank? }
.map { |v| "#{v[0]}: #{v[1]}" }
- .join(Constants::COMMA_SPACE)
+ .join(', '.freeze)
"Grouping <#{data}>"
end
diff --git a/lib/ransack/nodes/sort.rb b/lib/ransack/nodes/sort.rb
index 095a168a7..0a700df86 100644
--- a/lib/ransack/nodes/sort.rb
+++ b/lib/ransack/nodes/sort.rb
@@ -3,7 +3,7 @@ module Nodes
class Sort < Node
include Bindable
- attr_reader :name, :dir
+ attr_reader :name, :dir, :ransacker_args
i18n_word :asc, :desc
class << self
@@ -16,7 +16,7 @@ def extract(context, str)
def build(params)
params.with_indifferent_access.each do |key, value|
- if key.match(/^(name|dir)$/)
+ if key.match(/^(name|dir|ransacker_args)$/)
self.send("#{key}=", value)
end
end
@@ -38,13 +38,17 @@ def name=(name)
def dir=(dir)
dir = dir.downcase if dir
@dir =
- if Constants::ASC_DESC.include?(dir)
+ if ['asc'.freeze, 'desc'.freeze].freeze.include?(dir)
dir
else
- Constants::ASC
+ 'asc'.freeze
end
end
+ def ransacker_args=(ransack_args)
+ @ransacker_args = ransack_args
+ end
+
end
end
end
diff --git a/lib/ransack/predicate.rb b/lib/ransack/predicate.rb
index af1226bf5..2b5bd5da5 100644
--- a/lib/ransack/predicate.rb
+++ b/lib/ransack/predicate.rb
@@ -10,7 +10,7 @@ def names
end
def names_by_decreasing_length
- names.sort { |a,b| b.length <=> a.length }
+ names.sort { |a, b| b.length <=> a.length }
end
def named(name)
@@ -19,7 +19,7 @@ def named(name)
def detect_and_strip_from_string!(str)
if p = detect_from_string(str)
- str.sub! /_#{p}$/, Constants::EMPTY
+ str.sub! /_#{p}$/, ''.freeze
p
end
end
diff --git a/lib/ransack/search.rb b/lib/ransack/search.rb
index a5972b48e..bc97e0590 100644
--- a/lib/ransack/search.rb
+++ b/lib/ransack/search.rb
@@ -1,14 +1,6 @@
require 'ransack/nodes'
require 'ransack/context'
-
-if defined?(::ActiveRecord::Base)
- require 'ransack/adapters/active_record/ransack/context'
-end
-
-if defined?(::Mongoid)
- require 'ransack/adapters/mongoid/ransack/context'
-end
-
+Ransack::Adapters.require_search
require 'ransack/naming'
module Ransack
@@ -23,6 +15,7 @@ class Search
:translate, :to => :base
def initialize(object, params = {}, options = {})
+ params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
if params.is_a? Hash
params = params.dup
params.delete_if { |k, v| [*v].all?{ |i| i.blank? && i != false } }
@@ -45,7 +38,7 @@ def result(opts = {})
def build(params)
collapse_multiparameter_attributes!(params).each do |key, value|
- if Constants::S_SORTS.include?(key)
+ if ['s'.freeze, 'sorts'.freeze].freeze.include?(key)
send("#{key}=", value)
elsif base.attribute_method?(key)
base.send("#{key}=", value)
@@ -100,7 +93,7 @@ def new_sort(opts = {})
def method_missing(method_id, *args)
method_name = method_id.to_s
- getter_name = method_name.sub(/=$/, Constants::EMPTY)
+ getter_name = method_name.sub(/=$/, ''.freeze)
if base.attribute_method?(getter_name)
base.send(method_id, *args)
elsif @context.ransackable_scope?(getter_name, @context.object)
@@ -121,8 +114,8 @@ def inspect
[:base, base.inspect]
]
.compact
- .map { |d| d.join(Constants::COLON_SPACE) }
- .join(Constants::COMMA_SPACE)
+ .map { |d| d.join(': '.freeze) }
+ .join(', '.freeze)
"Ransack::Search<#{details}>"
end
diff --git a/lib/ransack/translate.rb b/lib/ransack/translate.rb
index ff3e884ce..93821ddd6 100644
--- a/lib/ransack/translate.rb
+++ b/lib/ransack/translate.rb
@@ -25,7 +25,7 @@ def self.attribute(key, options = {})
|x| x.respond_to?(:model_name)
}
predicate = Predicate.detect_from_string(original_name)
- attributes_str = original_name.sub(/_#{predicate}$/, Constants::EMPTY)
+ attributes_str = original_name.sub(/_#{predicate}$/, ''.freeze)
attribute_names = attributes_str.split(/_and_|_or_/)
combinator = attributes_str.match(/_and_/) ? :and : :or
defaults = base_ancestors.map do |klass|
@@ -74,7 +74,7 @@ def self.association(key, options = {})
def self.attribute_name(context, name, include_associations = nil)
@context, @name = context, name
@assoc_path = context.association_path(name)
- @attr_name = @name.sub(/^#{@assoc_path}_/, Constants::EMPTY)
+ @attr_name = @name.sub(/^#{@assoc_path}_/, ''.freeze)
associated_class = @context.traverse(@assoc_path) if @assoc_path.present?
@include_associated = include_associations && associated_class
diff --git a/lib/ransack/version.rb b/lib/ransack/version.rb
index 749ac2b7c..60501d82e 100644
--- a/lib/ransack/version.rb
+++ b/lib/ransack/version.rb
@@ -1,3 +1,3 @@
module Ransack
- VERSION = '1.6.6'
+ VERSION = '1.7.0'
end
diff --git a/ransack.gemspec b/ransack.gemspec
index a7d0b7508..f01daa3f2 100644
--- a/ransack.gemspec
+++ b/ransack.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.authors = ["Ernie Miller", "Ryan Bigg", "Jon Atack"]
s.email = ["ernie@erniemiller.org", "radarlistener@gmail.com", "jonnyatack@gmail.com"]
s.homepage = "https://github.com/activerecord-hackery/ransack"
- s.summary = %q{Object-based searching for ActiveRecord (currently).}
+ s.summary = %q{Object-based searching for Active Record and Mongoid (currently).}
s.description = %q{Ransack is the successor to the MetaSearch gem. It improves and expands upon MetaSearch's functionality, but does not have a 100%-compatible API.}
s.required_ruby_version = '>= 1.9'
s.license = 'MIT'
@@ -21,16 +21,15 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', '>= 3.0'
s.add_dependency 'i18n'
s.add_dependency 'polyamorous', '~> 1.2'
- s.add_development_dependency 'rspec', '~> 2.14.0'
+ s.add_development_dependency 'rspec', '~> 2'
s.add_development_dependency 'machinist', '~> 1.0.6'
s.add_development_dependency 'faker', '~> 0.9.5'
s.add_development_dependency 'sqlite3', '~> 1.3.3'
s.add_development_dependency 'pg'
- s.add_development_dependency 'mysql2', '0.3.14'
+ s.add_development_dependency 'mysql2', '0.3.20'
s.add_development_dependency 'pry', '0.9.12.2'
- s.files = `git ls-files`
- .split("\n")
+ s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`
.split("\n")
diff --git a/spec/mongoid/adapters/mongoid/base_spec.rb b/spec/mongoid/adapters/mongoid/base_spec.rb
index b3625a8b9..88cef22c6 100644
--- a/spec/mongoid/adapters/mongoid/base_spec.rb
+++ b/spec/mongoid/adapters/mongoid/base_spec.rb
@@ -50,6 +50,21 @@ module Mongoid
end
end
+ describe '#ransack_alias' do
+ it 'translates an alias to the correct attributes' do
+ p = Person.create!(name: 'Meatloaf', email: 'babies@example.com')
+
+ s = Person.ransack(term_cont: 'atlo')
+ expect(s.result.to_a).to eq [p]
+
+ s = Person.ransack(term_cont: 'babi')
+ expect(s.result.to_a).to eq [p]
+
+ s = Person.ransack(term_cont: 'nomatch')
+ expect(s.result.to_a).to eq []
+ end
+ end
+
describe '#ransacker' do
# For infix tests
def self.sane_adapter?
@@ -213,6 +228,7 @@ def self.sane_adapter?
it { should include 'name' }
it { should include 'reversed_name' }
it { should include 'doubled_name' }
+ it { should include 'term' }
it { should include 'only_search' }
it { should_not include 'only_sort' }
it { should_not include 'only_admin' }
@@ -224,6 +240,7 @@ def self.sane_adapter?
it { should include 'name' }
it { should include 'reversed_name' }
it { should include 'doubled_name' }
+ it { should include 'term' }
it { should include 'only_search' }
it { should_not include 'only_sort' }
it { should include 'only_admin' }
diff --git a/spec/mongoid/nodes/condition_spec.rb b/spec/mongoid/nodes/condition_spec.rb
index 7ab8e8459..8829ea89f 100644
--- a/spec/mongoid/nodes/condition_spec.rb
+++ b/spec/mongoid/nodes/condition_spec.rb
@@ -4,6 +4,21 @@ module Ransack
module Nodes
describe Condition do
+ context 'with an alias' do
+ subject {
+ Condition.extract(
+ Context.for(Person), 'term_start', Person.first(2).map(&:name)
+ )
+ }
+
+ specify { expect(subject.combinator).to eq 'or' }
+ specify { expect(subject.predicate.name).to eq 'start' }
+
+ it 'converts the alias to the correct attributes' do
+ expect(subject.attributes.map(&:name)).to eq(['name', 'email'])
+ end
+ end
+
context 'with multiple values and an _any predicate' do
subject { Condition.extract(Context.for(Person), 'name_eq_any', Person.first(2).map(&:name)) }
@@ -26,7 +41,7 @@ module Nodes
Ransack.configure { |config| config.ignore_unknown_conditions = true }
end
- specify { subject.should be_nil }
+ specify { expect(subject).to be_nil }
end
end
end
diff --git a/spec/mongoid/support/mongoid.yml b/spec/mongoid/support/mongoid.yml
index 999569418..dd169b0f2 100644
--- a/spec/mongoid/support/mongoid.yml
+++ b/spec/mongoid/support/mongoid.yml
@@ -1,4 +1,9 @@
test:
+ clients:
+ default:
+ database: ransack_mongoid_test
+ hosts:
+ - localhost:27017
sessions:
default:
database: ransack_mongoid_test
diff --git a/spec/mongoid/support/schema.rb b/spec/mongoid/support/schema.rb
index 7b3360482..6c15768ed 100644
--- a/spec/mongoid/support/schema.rb
+++ b/spec/mongoid/support/schema.rb
@@ -1,6 +1,8 @@
require 'mongoid'
Mongoid.load!(File.expand_path("../mongoid.yml", __FILE__), :test)
+Mongo::Logger.logger.level = Logger::WARN if defined?(Mongo)
+Mongoid.purge!
class Person
include Mongoid::Document
@@ -20,6 +22,8 @@ class Person
has_many :articles
has_many :comments
+ ransack_alias :term, :name_or_email
+
# has_many :authored_article_comments, :through => :articles,
# :source => :comments, :foreign_key => :person_id
diff --git a/spec/mongoid_spec_helper.rb b/spec/mongoid_spec_helper.rb
index 1d27b54f4..faffc0a00 100644
--- a/spec/mongoid_spec_helper.rb
+++ b/spec/mongoid_spec_helper.rb
@@ -9,10 +9,9 @@
Time.zone = 'Eastern Time (US & Canada)'
I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'support', '*.yml')]
-Dir[File.expand_path('../{mongoid/helpers,mongoid/support,blueprints}/*.rb', __FILE__)]
-.each do |f|
- require f
-end
+Dir[File.expand_path('../{mongoid/helpers,mongoid/support,blueprints}/*.rb',
+ __FILE__)]
+.each { |f| require f }
Sham.define do
name { Faker::Name.name }
@@ -31,11 +30,16 @@
config.alias_it_should_behave_like_to :it_has_behavior, 'has behavior'
config.before(:suite) do
- puts '=' * 80
- connection_name = Mongoid.default_session.inspect
- puts "Running specs against #{connection_name}, Mongoid #{
- Mongoid::VERSION}, Moped #{Moped::VERSION} and Origin #{Origin::VERSION}..."
- puts '=' * 80
+ if ENV['DB'] == 'mongoid4'
+ message = "Running Ransack specs with #{Mongoid.default_session.inspect
+ }, Mongoid #{Mongoid::VERSION}, Moped #{Moped::VERSION
+ }, Origin #{Origin::VERSION} and Ruby #{RUBY_VERSION}"
+ else
+ message = "Running Ransack specs with #{Mongoid.default_client.inspect
+ }, Mongoid #{Mongoid::VERSION}, Mongo driver #{Mongo::VERSION}"
+ end
+ line = '=' * message.length
+ puts line, message, line
Schema.create
end
diff --git a/spec/ransack/adapters/active_record/base_spec.rb b/spec/ransack/adapters/active_record/base_spec.rb
index 475870021..c9d3e97c7 100644
--- a/spec/ransack/adapters/active_record/base_spec.rb
+++ b/spec/ransack/adapters/active_record/base_spec.rb
@@ -20,53 +20,67 @@ module ActiveRecord
context 'with scopes' do
before do
- Person.stub :ransackable_scopes => [:active, :over_age, :of_age]
+ Person.stub ransackable_scopes: [:active, :over_age, :of_age]
end
- it "applies true scopes" do
+ it 'applies true scopes' do
s = Person.ransack('active' => true)
- s.result.to_sql.should include "active = 1"
+ expect(s.result.to_sql).to (include 'active = 1')
end
- it "applies stringy true scopes" do
+ it 'applies stringy true scopes' do
s = Person.ransack('active' => 'true')
- s.result.to_sql.should include "active = 1"
+ expect(s.result.to_sql).to (include 'active = 1')
end
- it "applies stringy boolean scopes with true value in an array" do
+ it 'applies stringy boolean scopes with true value in an array' do
s = Person.ransack('of_age' => ['true'])
- s.result.to_sql.should include "age >= 18"
+ expect(s.result.to_sql).to (include 'age >= 18')
end
- it "applies stringy boolean scopes with false value in an array" do
+ it 'applies stringy boolean scopes with false value in an array' do
s = Person.ransack('of_age' => ['false'])
- s.result.to_sql.should include "age < 18"
+ expect(s.result.to_sql).to (include 'age < 18')
end
- it "ignores unlisted scopes" do
+ it 'ignores unlisted scopes' do
s = Person.ransack('restricted' => true)
- s.result.to_sql.should_not include "restricted"
+ expect(s.result.to_sql).to_not (include 'restricted')
end
- it "ignores false scopes" do
+ it 'ignores false scopes' do
s = Person.ransack('active' => false)
- s.result.to_sql.should_not include "active"
+ expect(s.result.to_sql).not_to (include 'active')
end
- it "ignores stringy false scopes" do
+ it 'ignores stringy false scopes' do
s = Person.ransack('active' => 'false')
- s.result.to_sql.should_not include "active"
+ expect(s.result.to_sql).to_not (include 'active')
end
- it "passes values to scopes" do
+ it 'passes values to scopes' do
s = Person.ransack('over_age' => 18)
- s.result.to_sql.should include "age > 18"
+ expect(s.result.to_sql).to (include 'age > 18')
end
- it "chains scopes" do
+ # TODO: Implement a way to pass true/false values like 0 or 1 to
+ # scopes (e.g. with `in` / `not_in` predicates), without Ransack
+ # converting them to true/false boolean values instead.
+
+ # it 'passes true values to scopes', focus: true do
+ # s = Person.ransack('over_age' => 1)
+ # expect(s.result.to_sql).to (include 'age > 1')
+ # end
+
+ # it 'passes false values to scopes', focus: true do
+ # s = Person.ransack('over_age' => 0)
+ # expect(s.result.to_sql).to (include 'age > 0')
+ # end
+
+ it 'chains scopes' do
s = Person.ransack('over_age' => 18, 'active' => true)
- s.result.to_sql.should include "age > 18"
- s.result.to_sql.should include "active = 1"
+ expect(s.result.to_sql).to (include 'age > 18')
+ expect(s.result.to_sql).to (include 'active = 1')
end
end
@@ -75,16 +89,42 @@ module ActiveRecord
end
it 'does not modify the parameters' do
- params = { :name_eq => '' }
+ params = { name_eq: '' }
expect { Person.ransack(params) }.not_to change { params }
end
end
+ describe '#ransack_alias' do
+ it 'translates an alias to the correct attributes' do
+ p = Person.create!(name: 'Meatloaf', email: 'babies@example.com')
+
+ s = Person.ransack(term_cont: 'atlo')
+ expect(s.result.to_a).to eq [p]
+
+ s = Person.ransack(term_cont: 'babi')
+ expect(s.result.to_a).to eq [p]
+
+ s = Person.ransack(term_cont: 'nomatch')
+ expect(s.result.to_a).to eq []
+ end
+
+ it 'also works with associations' do
+ dad = Person.create!(name: 'Birdman')
+ son = Person.create!(name: 'Weezy', parent: dad)
+
+ s = Person.ransack(daddy_eq: 'Birdman')
+ expect(s.result.to_a).to eq [son]
+
+ s = Person.ransack(daddy_eq: 'Drake')
+ expect(s.result.to_a).to eq []
+ end
+ end
+
describe '#ransacker' do
# For infix tests
def self.sane_adapter?
case ::ActiveRecord::Base.connection.adapter_name
- when "SQLite3", "PostgreSQL"
+ when 'SQLite3', 'PostgreSQL'
true
else
false
@@ -102,84 +142,111 @@ def self.sane_adapter?
# end
it 'creates ransack attributes' do
- s = Person.ransack(:reversed_name_eq => 'htimS cirA')
+ s = Person.ransack(reversed_name_eq: 'htimS cirA')
expect(s.result.size).to eq(1)
expect(s.result.first).to eq Person.where(name: 'Aric Smith').first
end
it 'can be accessed through associations' do
- s = Person.ransack(:children_reversed_name_eq => 'htimS cirA')
+ s = Person.ransack(children_reversed_name_eq: 'htimS cirA')
expect(s.result.to_sql).to match(
/#{quote_table_name("children_people")}.#{
quote_column_name("name")} = 'Aric Smith'/
)
end
- it 'allows an "attribute" to be an InfixOperation' do
- s = Person.ransack(:doubled_name_eq => 'Aric SmithAric Smith')
+ it 'allows an attribute to be an InfixOperation' do
+ s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
expect(s.result.first).to eq Person.where(name: 'Aric Smith').first
end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
- it "doesn't break #count if using InfixOperations" do
- s = Person.ransack(:doubled_name_eq => 'Aric SmithAric Smith')
+ it 'does not break #count if using InfixOperations' do
+ s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
expect(s.result.count).to eq 1
end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
- it "should remove empty key value pairs from the params hash" do
- s = Person.ransack(:children_reversed_name_eq => '')
+ it 'should remove empty key value pairs from the params hash' do
+ s = Person.ransack(children_reversed_name_eq: '')
expect(s.result.to_sql).not_to match /LEFT OUTER JOIN/
end
- it "should keep proper key value pairs in the params hash" do
- s = Person.ransack(:children_reversed_name_eq => 'Testing')
+ it 'should keep proper key value pairs in the params hash' do
+ s = Person.ransack(children_reversed_name_eq: 'Testing')
expect(s.result.to_sql).to match /LEFT OUTER JOIN/
end
- it "should function correctly when nil is passed in" do
+ it 'should function correctly when nil is passed in' do
s = Person.ransack(nil)
end
- it "should function correctly when a blank string is passed in" do
+ it 'should function correctly when a blank string is passed in' do
s = Person.ransack('')
end
- it "should function correctly with a multi-parameter attribute" do
+ it 'should function correctly with a multi-parameter attribute' do
+ ::ActiveRecord::Base.default_timezone = :utc
+ Time.zone = 'UTC'
+
date = Date.current
s = Person.ransack(
- { "created_at_gteq(1i)" => date.year,
- "created_at_gteq(2i)" => date.month,
- "created_at_gteq(3i)" => date.day
+ { 'created_at_gteq(1i)' => date.year,
+ 'created_at_gteq(2i)' => date.month,
+ 'created_at_gteq(3i)' => date.day
}
)
expect(s.result.to_sql).to match />=/
expect(s.result.to_sql).to match date.to_s
end
- it "should function correctly when using fields with dots in them" do
- s = Person.ransack(:email_cont => "example.com")
+ it 'should function correctly when using fields with dots in them' do
+ s = Person.ransack(email_cont: 'example.com')
expect(s.result.exists?).to be true
end
- it "should function correctly when using fields with % in them" do
- p = Person.create!(:name => "110%-er")
- s = Person.ransack(:name_cont => "10%")
+ it 'should function correctly when using fields with % in them' do
+ p = Person.create!(name: '110%-er')
+ s = Person.ransack(name_cont: '10%')
expect(s.result.to_a).to eq [p]
end
- it "should function correctly when using fields with backslashes in them" do
- p = Person.create!(:name => "\\WINNER\\")
- s = Person.ransack(:name_cont => "\\WINNER\\")
+ it 'should function correctly when using fields with backslashes in them' do
+ p = Person.create!(name: "\\WINNER\\")
+ s = Person.ransack(name_cont: "\\WINNER\\")
expect(s.result.to_a).to eq [p]
end
- context "searching on an `in` predicate with a ransacker" do
- it "should function correctly when passing an array of ids" do
+ context 'searching by underscores' do
+ # when escaping is supported right in LIKE expression without adding extra expressions
+ def self.simple_escaping?
+ case ::ActiveRecord::Base.connection.adapter_name
+ when 'Mysql2', 'PostgreSQL'
+ true
+ else
+ false
+ end
+ end
+
+ it 'should search correctly if matches exist' do
+ p = Person.create!(name: 'name_with_underscore')
+ s = Person.ransack(name_cont: 'name_')
+ expect(s.result.to_a).to eq [p]
+ end if simple_escaping?
+
+ it 'should return empty result if no matches' do
+ Person.create!(name: 'name_with_underscore')
+ s = Person.ransack(name_cont: 'n_')
+ expect(s.result.to_a).to eq []
+ end if simple_escaping?
+ end
+
+ context 'searching on an `in` predicate with a ransacker' do
+ it 'should function correctly when passing an array of ids' do
s = Person.ransack(array_users_in: true)
expect(s.result.count).to be > 0
end
- it "should function correctly when passing an array of strings" do
+ it 'should function correctly when passing an array of strings' do
Person.create!(name: Person.first.id.to_s)
s = Person.ransack(array_names_in: true)
expect(s.result.count).to be > 0
@@ -193,54 +260,67 @@ def self.sane_adapter?
end
end
- context "search on an `in` predicate with an array" do
- it "should function correctly when passing an array of ids" do
+ context 'search on an `in` predicate with an array' do
+ it 'should function correctly when passing an array of ids' do
array = Person.all.map(&:id)
s = Person.ransack(id_in: array)
expect(s.result.count).to eq array.size
end
end
- it "should function correctly when an attribute name ends with '_start'" do
- p = Person.create!(:new_start => 'Bar and foo', :name => 'Xiang')
+ it 'should work correctly when an attribute name ends with _start' do
+ p = Person.create!(new_start: 'Bar and foo', name: 'Xiang')
- s = Person.ransack(:new_start_end => ' and foo')
+ s = Person.ransack(new_start_end: ' and foo')
expect(s.result.to_a).to eq [p]
- s = Person.ransack(:name_or_new_start_start => 'Xia')
+ s = Person.ransack(name_or_new_start_start: 'Xia')
expect(s.result.to_a).to eq [p]
- s = Person.ransack(:new_start_or_name_end => 'iang')
+ s = Person.ransack(new_start_or_name_end: 'iang')
expect(s.result.to_a).to eq [p]
end
- it "should function correctly when an attribute name ends with '_end'" do
- p = Person.create!(:stop_end => 'Foo and bar', :name => 'Marianne')
+ it 'should work correctly when an attribute name ends with _end' do
+ p = Person.create!(stop_end: 'Foo and bar', name: 'Marianne')
- s = Person.ransack(:stop_end_start => 'Foo and')
+ s = Person.ransack(stop_end_start: 'Foo and')
expect(s.result.to_a).to eq [p]
- s = Person.ransack(:stop_end_or_name_end => 'anne')
+ s = Person.ransack(stop_end_or_name_end: 'anne')
expect(s.result.to_a).to eq [p]
- s = Person.ransack(:name_or_stop_end_end => ' bar')
+ s = Person.ransack(name_or_stop_end_end: ' bar')
expect(s.result.to_a).to eq [p]
end
- it "should function correctly when an attribute name has 'and' in it" do
- # FIXME: this test does not pass!
- p = Person.create!(:terms_and_conditions => true)
- s = Person.ransack(:terms_and_conditions_eq => true)
- # search is not detecting the attribute
- puts "
- FIXME: Search not detecting the `terms_and_conditions` attribute in
- base_spec.rb, line 178: #{s.result.to_sql}"
- # expect(s.result.to_a).to eq [p]
+ it 'should work correctly when an attribute name has `and` in it' do
+ p = Person.create!(terms_and_conditions: true)
+ s = Person.ransack(terms_and_conditions_eq: true)
+ expect(s.result.to_a).to eq [p]
end
- it 'allows sort by "only_sort" field' do
+ context 'attribute aliased column names',
+ if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
+ it 'should be translated to original column name' do
+ s = Person.ransack(full_name_eq: 'Nicolas Cage')
+ expect(s.result.to_sql).to match(
+ /WHERE #{quote_table_name("people")}.#{quote_column_name("name")}/
+ )
+ end
+
+ it 'should translate on associations' do
+ s = Person.ransack(articles_content_cont: 'Nicolas Cage')
+ expect(s.result.to_sql).to match(
+ /#{quote_table_name("articles")}.#{
+ quote_column_name("body")} I?LIKE '%Nicolas Cage%'/
+ )
+ end
+ end
+
+ it 'allows sort by `only_sort` field' do
s = Person.ransack(
- "s" => { "0" => { "dir" => "asc", "name" => "only_sort" } }
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_sort' } }
)
expect(s.result.to_sql).to match(
/ORDER BY #{quote_table_name("people")}.#{
@@ -248,9 +328,9 @@ def self.sane_adapter?
)
end
- it "doesn't sort by 'only_search' field" do
+ it 'does not sort by `only_search` field' do
s = Person.ransack(
- "s" => { "0" => { "dir" => "asc", "name" => "only_search" } }
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_search' } }
)
expect(s.result.to_sql).not_to match(
/ORDER BY #{quote_table_name("people")}.#{
@@ -258,25 +338,25 @@ def self.sane_adapter?
)
end
- it 'allows search by "only_search" field' do
- s = Person.ransack(:only_search_eq => 'htimS cirA')
+ it 'allows search by `only_search` field' do
+ s = Person.ransack(only_search_eq: 'htimS cirA')
expect(s.result.to_sql).to match(
/WHERE #{quote_table_name("people")}.#{
quote_column_name("only_search")} = 'htimS cirA'/
)
end
- it "can't be searched by 'only_sort'" do
- s = Person.ransack(:only_sort_eq => 'htimS cirA')
+ it 'cannot be searched by `only_sort`' do
+ s = Person.ransack(only_sort_eq: 'htimS cirA')
expect(s.result.to_sql).not_to match(
/WHERE #{quote_table_name("people")}.#{
quote_column_name("only_sort")} = 'htimS cirA'/
)
end
- it 'allows sort by "only_admin" field, if auth_object: :admin' do
+ it 'allows sort by `only_admin` field, if auth_object: :admin' do
s = Person.ransack(
- { "s" => { "0" => { "dir" => "asc", "name" => "only_admin" } } },
+ { 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } } },
{ auth_object: :admin }
)
expect(s.result.to_sql).to match(
@@ -285,9 +365,9 @@ def self.sane_adapter?
)
end
- it "doesn't sort by 'only_admin' field, if auth_object: nil" do
+ it 'does not sort by `only_admin` field, if auth_object: nil' do
s = Person.ransack(
- "s" => { "0" => { "dir" => "asc", "name" => "only_admin" } }
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } }
)
expect(s.result.to_sql).not_to match(
/ORDER BY #{quote_table_name("people")}.#{
@@ -295,10 +375,10 @@ def self.sane_adapter?
)
end
- it 'allows search by "only_admin" field, if auth_object: :admin' do
+ it 'allows search by `only_admin` field, if auth_object: :admin' do
s = Person.ransack(
- { :only_admin_eq => 'htimS cirA' },
- { :auth_object => :admin }
+ { only_admin_eq: 'htimS cirA' },
+ { auth_object: :admin }
)
expect(s.result.to_sql).to match(
/WHERE #{quote_table_name("people")}.#{
@@ -306,8 +386,8 @@ def self.sane_adapter?
)
end
- it "can't be searched by 'only_admin', if auth_object: nil" do
- s = Person.ransack(:only_admin_eq => 'htimS cirA')
+ it 'cannot be searched by `only_admin`, if auth_object: nil' do
+ s = Person.ransack(only_admin_eq: 'htimS cirA')
expect(s.result.to_sql).not_to match(
/WHERE #{quote_table_name("people")}.#{
quote_column_name("only_admin")} = 'htimS cirA'/
@@ -334,6 +414,25 @@ def self.sane_adapter?
)
expect { s.result.first }.to_not raise_error
end
+
+ it 'should allow sort passing arguments to a ransacker' do
+ s = Person.ransack(
+ s: {
+ '0' => {
+ name: 'with_arguments', dir: 'desc', ransacker_args: [2, 6]
+ }
+ }
+ )
+ expect(s.result.to_sql).to match(
+ /ORDER BY \(SELECT MAX\(articles.title\) FROM articles/
+ )
+ expect(s.result.to_sql).to match(
+ /WHERE articles.person_id = people.id AND LENGTH\(articles.body\)/
+ )
+ expect(s.result.to_sql).to match(
+ /BETWEEN 2 AND 6 GROUP BY articles.person_id \) DESC/
+ )
+ end
end
describe '#ransackable_attributes' do
@@ -343,9 +442,14 @@ def self.sane_adapter?
it { should include 'name' }
it { should include 'reversed_name' }
it { should include 'doubled_name' }
+ it { should include 'term' }
it { should include 'only_search' }
it { should_not include 'only_sort' }
it { should_not include 'only_admin' }
+
+ if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
+ it { should include 'full_name' }
+ end
end
context 'with auth_object :admin' do
diff --git a/spec/ransack/adapters/active_record/context_spec.rb b/spec/ransack/adapters/active_record/context_spec.rb
index cb21bc532..c25128dcf 100644
--- a/spec/ransack/adapters/active_record/context_spec.rb
+++ b/spec/ransack/adapters/active_record/context_spec.rb
@@ -9,11 +9,11 @@ module ActiveRecord
describe Context do
subject { Context.new(Person) }
- if AR_version >= '3.1'
- it 'has an Active Record alias tracker method' do
- expect(subject.alias_tracker)
- .to be_an ::ActiveRecord::Associations::AliasTracker
- end
+
+ it 'has an Active Record alias tracker method',
+ if: AR_version >= '3.1' do
+ expect(subject.alias_tracker)
+ .to be_an ::ActiveRecord::Associations::AliasTracker
end
describe '#relation_for' do
@@ -24,8 +24,8 @@ module ActiveRecord
describe '#evaluate' do
it 'evaluates search objects' do
- search = Search.new(Person, :name_eq => 'Joe Blow')
- result = subject.evaluate(search)
+ s = Search.new(Person, name_eq: 'Joe Blow')
+ result = subject.evaluate(s)
expect(result).to be_an ::ActiveRecord::Relation
expect(result.to_sql)
@@ -33,25 +33,25 @@ module ActiveRecord
end
it 'SELECTs DISTINCT when distinct: true' do
- search = Search.new(Person, :name_eq => 'Joe Blow')
- result = subject.evaluate(search, :distinct => true)
+ s = Search.new(Person, name_eq: 'Joe Blow')
+ result = subject.evaluate(s, distinct: true)
expect(result).to be_an ::ActiveRecord::Relation
expect(result.to_sql).to match /SELECT DISTINCT/
end
end
- describe "sharing context across searches" do
+ describe 'sharing context across searches' do
let(:shared_context) { Context.for(Person) }
before do
- Search.new(Person, { :parent_name_eq => 'A' },
+ Search.new(Person, { parent_name_eq: 'A' },
context: shared_context)
- Search.new(Person, { :children_name_eq => 'B' },
+ Search.new(Person, { children_name_eq: 'B' },
context: shared_context)
end
- describe '#join_associations', :if => AR_version <= '4.0' 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
@@ -69,18 +69,16 @@ module ActiveRecord
end
describe '#join_sources' do
- # FIXME: fix this test for Rails 4.2.
+ # FIXME: fix this test for Rails 4.2 and 5.0.
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
+ 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',
- :if => AR_version >= '3.1' do
+ if: AR_version >= '3.1' do
parents, children = shared_context.join_sources
expect { Person.joins(parents).joins(children).to_a }
diff --git a/spec/ransack/dependencies_spec.rb b/spec/ransack/dependencies_spec.rb
index 0bb137806..8d32fa44d 100644
--- a/spec/ransack/dependencies_spec.rb
+++ b/spec/ransack/dependencies_spec.rb
@@ -1,3 +1,4 @@
+=begin
rails = ::ActiveRecord::VERSION::STRING.first(3)
if %w(3.2 4.0 4.1).include?(rails) || rails == '3.1' && RUBY_VERSION < '2.2'
@@ -8,3 +9,4 @@
end
end
end
+=end
diff --git a/spec/ransack/helpers/form_builder_spec.rb b/spec/ransack/helpers/form_builder_spec.rb
index 2a19ee7e1..0f380643a 100644
--- a/spec/ransack/helpers/form_builder_spec.rb
+++ b/spec/ransack/helpers/form_builder_spec.rb
@@ -22,7 +22,11 @@ module Helpers
@controller.view_context.search_form_for(@s) { |f| @f = f }
end
- it 'selects previously-entered time values with datetime_select' do
+ it 'selects previously-entered time values with datetime_select',
+ unless: (
+ RUBY_VERSION >= '2.3' &&
+ ::ActiveRecord::VERSION::STRING.first(3) < '3.2'
+ ) do
date_values = %w(2011 1 2 03 04 05)
# @s.created_at_eq = date_values # This works in Rails 4.x but not 3.x
@s.created_at_eq = [2011, 1, 2, 3, 4, 5] # so we have to do this
diff --git a/spec/ransack/helpers/form_helper_spec.rb b/spec/ransack/helpers/form_helper_spec.rb
index 261b95fb5..7dc28d112 100644
--- a/spec/ransack/helpers/form_helper_spec.rb
+++ b/spec/ransack/helpers/form_helper_spec.rb
@@ -19,13 +19,8 @@ module Helpers
before do
@controller = ActionView::TestCase::TestController.new
@controller.instance_variable_set(:@_routes, router)
- @controller.class_eval do
- include router.url_helpers
- end
-
- @controller.view_context_class.class_eval do
- include router.url_helpers
- end
+ @controller.class_eval { include router.url_helpers }
+ @controller.view_context_class.class_eval { include router.url_helpers }
end
describe '#sort_link with default search_key' do
@@ -344,6 +339,7 @@ module Helpers
)
}
it { should match /Full Name/ }
+ it { should_not match /▼|▲/ }
end
describe '#sort_link with hide order indicator set to false' do
@@ -358,6 +354,45 @@ module Helpers
it { should match /Full Name ▼/ }
end
+ describe '#sort_link with config set to globally hide order indicators' do
+ before do
+ Ransack.configure { |c| c.hide_sort_order_indicators = true }
+ end
+ subject { @controller.view_context
+ .sort_link(
+ [:main_app, Person.search(sorts: ['name desc'])],
+ :name,
+ controller: 'people'
+ )
+ }
+ it { should_not match /▼|▲/ }
+ end
+
+ describe '#sort_link with config set to globally show order indicators' do
+ before do
+ Ransack.configure { |c| c.hide_sort_order_indicators = false }
+ end
+ subject { @controller.view_context
+ .sort_link(
+ [:main_app, Person.search(sorts: ['name desc'])],
+ :name,
+ controller: 'people'
+ )
+ }
+ it { should match /Full Name ▼/ }
+ end
+
+ describe '#sort_link with a block' do
+ subject { @controller.view_context
+ .sort_link(
+ [:main_app, Person.search(sorts: ['name desc'])],
+ :name,
+ controller: 'people'
+ ) { 'Block label' }
+ }
+ it { should match /Block label ▼/ }
+ end
+
describe '#search_form_for with default format' do
subject { @controller.view_context
.search_form_for(Person.search) {} }
@@ -398,7 +433,6 @@ module Helpers
}
it { should match /example_name_eq/ }
end
-
end
end
end
diff --git a/spec/ransack/nodes/condition_spec.rb b/spec/ransack/nodes/condition_spec.rb
index 2c5de00fe..6cb728ddc 100644
--- a/spec/ransack/nodes/condition_spec.rb
+++ b/spec/ransack/nodes/condition_spec.rb
@@ -4,6 +4,21 @@ module Ransack
module Nodes
describe Condition do
+ context 'with an alias' do
+ subject {
+ Condition.extract(
+ Context.for(Person), 'term_start', Person.first(2).map(&:name)
+ )
+ }
+
+ specify { expect(subject.combinator).to eq 'or' }
+ specify { expect(subject.predicate.name).to eq 'start' }
+
+ it 'converts the alias to the correct attributes' do
+ expect(subject.attributes.map(&:name)).to eq(['name', 'email'])
+ end
+ end
+
context 'with multiple values and an _any predicate' do
subject {
Condition.extract(
@@ -34,7 +49,7 @@ module Nodes
Ransack.configure { |c| c.ignore_unknown_conditions = true }
end
- specify { subject.should be_nil }
+ specify { expect(subject).to be_nil }
end
end
end
diff --git a/spec/ransack/predicate_spec.rb b/spec/ransack/predicate_spec.rb
index f3b893bb6..3bc10bb33 100644
--- a/spec/ransack/predicate_spec.rb
+++ b/spec/ransack/predicate_spec.rb
@@ -16,7 +16,7 @@ module Ransack
expect { subject.result }.to_not raise_error
end
- it "escapes '%', '.' and '\\\\' in value" do
+ it "escapes '%', '.', '_' and '\\\\' in value" do
subject.send(:"#{method}=", '%._\\')
expect(subject.result.to_sql).to match(regexp)
end
@@ -124,9 +124,9 @@ module Ransack
describe 'cont' do
it_has_behavior 'wildcard escaping', :name_cont,
(if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
- /"people"."name" ILIKE '%\\%\\._\\\\%'/
+ /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
- /`people`.`name` LIKE '%\\\\%\\\\._\\\\\\\\%'/
+ /`people`.`name` LIKE '%\\\\%\\\\.\\\\_\\\\\\\\%'/
else
/"people"."name" LIKE '%%._\\%'/
end) do
@@ -143,9 +143,9 @@ module Ransack
describe 'not_cont' do
it_has_behavior 'wildcard escaping', :name_not_cont,
(if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
- /"people"."name" NOT ILIKE '%\\%\\._\\\\%'/
+ /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
- /`people`.`name` NOT LIKE '%\\\\%\\\\._\\\\\\\\%'/
+ /`people`.`name` NOT LIKE '%\\\\%\\\\.\\\\_\\\\\\\\%'/
else
/"people"."name" NOT LIKE '%%._\\%'/
end) do
diff --git a/spec/ransack/search_spec.rb b/spec/ransack/search_spec.rb
index 779fba8f6..a73dc554d 100644
--- a/spec/ransack/search_spec.rb
+++ b/spec/ransack/search_spec.rb
@@ -43,16 +43,16 @@ module Ransack
it 'accepts a context option' do
shared_context = Context.for(Person)
- search1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
- search2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
- expect(search1.context).to be search2.context
+ s1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
+ s2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
+ expect(s1.context).to be s2.context
end
end
describe '#build' do
it 'creates conditions for top-level attributes' do
- search = Search.new(Person, name_eq: 'Ernie')
- condition = search.base[:name_eq]
+ s = Search.new(Person, name_eq: 'Ernie')
+ condition = s.base[:name_eq]
expect(condition).to be_a Nodes::Condition
expect(condition.predicate.name).to eq 'eq'
expect(condition.attributes.first.name).to eq 'name'
@@ -60,8 +60,8 @@ module Ransack
end
it 'creates conditions for association attributes' do
- search = Search.new(Person, children_name_eq: 'Ernie')
- condition = search.base[:children_name_eq]
+ s = Search.new(Person, children_name_eq: 'Ernie')
+ condition = s.base[:children_name_eq]
expect(condition).to be_a Nodes::Condition
expect(condition.predicate.name).to eq 'eq'
expect(condition.attributes.first.name).to eq 'children_name'
@@ -69,8 +69,8 @@ module Ransack
end
it 'creates conditions for polymorphic belongs_to association attributes' do
- search = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
- condition = search.base[:notable_of_Person_type_name_eq]
+ s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
+ condition = s.base[:notable_of_Person_type_name_eq]
expect(condition).to be_a Nodes::Condition
expect(condition.predicate.name).to eq 'eq'
expect(condition.attributes.first.name)
@@ -80,9 +80,9 @@ module Ransack
it 'creates conditions for multiple polymorphic belongs_to association
attributes' do
- search = Search.new(Note,
+ s = Search.new(Note,
notable_of_Person_type_name_or_notable_of_Article_type_title_eq: 'Ernie')
- condition = search.
+ condition = s.
base[:notable_of_Person_type_name_or_notable_of_Article_type_title_eq]
expect(condition).to be_a Nodes::Condition
expect(condition.predicate.name).to eq 'eq'
@@ -93,15 +93,25 @@ module Ransack
expect(condition.value).to eq 'Ernie'
end
+ it 'creates conditions for aliased attributes',
+ if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
+ s = Search.new(Person, full_name_eq: 'Ernie')
+ condition = s.base[:full_name_eq]
+ expect(condition).to be_a Nodes::Condition
+ expect(condition.predicate.name).to eq 'eq'
+ expect(condition.attributes.first.name).to eq 'full_name'
+ expect(condition.value).to eq 'Ernie'
+ end
+
it 'preserves default scope and conditions for associations' do
- search = Search.new(Person, published_articles_title_eq: 'Test')
- expect(search.result.to_sql).to include 'default_scope'
- expect(search.result.to_sql).to include 'published'
+ s = Search.new(Person, published_articles_title_eq: 'Test')
+ expect(s.result.to_sql).to include 'default_scope'
+ expect(s.result.to_sql).to include 'published'
end
it 'discards empty conditions' do
- search = Search.new(Person, children_name_eq: '')
- condition = search.base[:children_name_eq]
+ s = Search.new(Person, children_name_eq: '')
+ condition = s.base[:children_name_eq]
expect(condition).to be_nil
end
@@ -111,13 +121,13 @@ module Ransack
end
it 'accepts arrays of groupings' do
- search = Search.new(Person,
+ s = Search.new(Person,
g: [
{ m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
{ m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
]
)
- ors = search.groupings
+ ors = s.groupings
expect(ors.size).to eq(2)
or1, or2 = ors
expect(or1).to be_a Nodes::Grouping
@@ -126,14 +136,14 @@ module Ransack
expect(or2.combinator).to eq 'or'
end
- it 'accepts "attributes" hashes for groupings' do
- search = Search.new(Person,
+ it 'accepts attributes hashes for groupings' do
+ s = Search.new(Person,
g: {
'0' => { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
'1' => { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
}
)
- ors = search.groupings
+ ors = s.groupings
expect(ors.size).to eq(2)
or1, or2 = ors
expect(or1).to be_a Nodes::Grouping
@@ -142,8 +152,8 @@ module Ransack
expect(or2.combinator).to eq 'or'
end
- it 'accepts "attributes" hashes for conditions' do
- search = Search.new(Person,
+ it 'accepts attributes hashes for conditions' do
+ s = Search.new(Person,
c: {
'0' => { a: ['name'], p: 'eq', v: ['Ernie'] },
'1' => {
@@ -152,7 +162,7 @@ module Ransack
}
}
)
- conditions = search.base.conditions
+ conditions = s.base.conditions
expect(conditions.size).to eq(2)
expect(conditions.map { |c| c.class })
.to eq [Nodes::Condition, Nodes::Condition]
@@ -163,8 +173,8 @@ module Ransack
config.add_predicate 'ary_pred', wants_array: true
end
- search = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
- condition = search.base[:name_ary_pred]
+ s = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
+ condition = s.base[:name_ary_pred]
expect(condition).to be_a Nodes::Condition
expect(condition.predicate.name).to eq 'ary_pred'
expect(condition.attributes.first.name).to eq 'name'
@@ -172,8 +182,8 @@ module Ransack
end
it 'does not evaluate the query on #inspect' do
- search = Search.new(Person, children_id_in: [1, 2, 3])
- expect(search.inspect).not_to match /ActiveRecord/
+ s = Search.new(Person, children_id_in: [1, 2, 3])
+ expect(s.inspect).not_to match /ActiveRecord/
end
context 'with an invalid condition' do
@@ -211,9 +221,9 @@ module Ransack
"#{quote_table_name("children_people")}.#{quote_column_name("name")}"
}
it 'evaluates conditions contextually' do
- search = Search.new(Person, children_name_eq: 'Ernie')
- expect(search.result).to be_an ActiveRecord::Relation
- expect(search.result.to_sql).to match /#{
+ s = Search.new(Person, children_name_eq: 'Ernie')
+ expect(s.result).to be_an ActiveRecord::Relation
+ expect(s.result.to_sql).to match /#{
children_people_name_field} = 'Ernie'/
end
@@ -221,51 +231,50 @@ module Ransack
# commenting out lines 221 and 242 to run the test. Addresses issue #374.
# https://github.com/activerecord-hackery/ransack/issues/374
#
- if ::ActiveRecord::VERSION::STRING.first(3) == '4.0'
- it 'evaluates conditions for multiple belongs_to associations to the
- same table contextually' do
- s = Search.new(Recommendation,
- person_name_eq: 'Ernie',
- target_person_parent_name_eq: 'Test'
- ).result
- expect(s).to be_an ActiveRecord::Relation
- real_query = remove_quotes_and_backticks(s.to_sql)
- expected_query = <<-SQL
- SELECT recommendations.* FROM recommendations
- LEFT OUTER JOIN people ON people.id = recommendations.person_id
- LEFT OUTER JOIN people target_people_recommendations
- ON target_people_recommendations.id = recommendations.target_person_id
- LEFT OUTER JOIN people parents_people
- ON parents_people.id = target_people_recommendations.parent_id
- WHERE ((people.name = 'Ernie' AND parents_people.name = 'Test'))
- SQL
- .squish
- expect(real_query).to eq expected_query
- end
+ it 'evaluates conditions for multiple `belongs_to` associations to the
+ same table contextually',
+ if: ::ActiveRecord::VERSION::STRING.first(3) == '4.0' do
+ s = Search.new(
+ Recommendation,
+ person_name_eq: 'Ernie',
+ target_person_parent_name_eq: 'Test'
+ ).result
+ expect(s).to be_an ActiveRecord::Relation
+ real_query = remove_quotes_and_backticks(s.to_sql)
+ expected_query = <<-SQL
+ SELECT recommendations.* FROM recommendations
+ LEFT OUTER JOIN people ON people.id = recommendations.person_id
+ LEFT OUTER JOIN people target_people_recommendations
+ ON target_people_recommendations.id = recommendations.target_person_id
+ LEFT OUTER JOIN people parents_people
+ ON parents_people.id = target_people_recommendations.parent_id
+ WHERE ((people.name = 'Ernie' AND parents_people.name = 'Test'))
+ SQL
+ .squish
+ expect(real_query).to eq expected_query
end
it 'evaluates compound conditions contextually' do
- search = Search.new(Person, children_name_or_name_eq: 'Ernie').result
- expect(search).to be_an ActiveRecord::Relation
- expect(search.to_sql).to match /#{children_people_name_field
+ s = Search.new(Person, children_name_or_name_eq: 'Ernie').result
+ expect(s).to be_an ActiveRecord::Relation
+ expect(s.to_sql).to match /#{children_people_name_field
} = 'Ernie' OR #{people_name_field} = 'Ernie'/
end
it 'evaluates polymorphic belongs_to association conditions contextually' do
- search = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
- .result
- expect(search).to be_an ActiveRecord::Relation
- expect(search.to_sql).to match /#{people_name_field} = 'Ernie'/
+ s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie').result
+ expect(s).to be_an ActiveRecord::Relation
+ expect(s.to_sql).to match /#{people_name_field} = 'Ernie'/
end
it 'evaluates nested conditions' do
- search = Search.new(Person, children_name_eq: 'Ernie',
+ s = Search.new(Person, children_name_eq: 'Ernie',
g: [
{ m: 'or', name_eq: 'Ernie', children_children_name_eq: 'Ernie' }
]
).result
- expect(search).to be_an ActiveRecord::Relation
- first, last = search.to_sql.split(/ AND /)
+ expect(s).to be_an ActiveRecord::Relation
+ first, last = s.to_sql.split(/ AND /)
expect(first).to match /#{children_people_name_field} = 'Ernie'/
expect(last).to match /#{
people_name_field} = 'Ernie' OR #{
@@ -274,14 +283,14 @@ module Ransack
end
it 'evaluates arrays of groupings' do
- search = Search.new(Person,
+ s = Search.new(Person,
g: [
{ m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
{ m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' }
]
).result
- expect(search).to be_an ActiveRecord::Relation
- first, last = search.to_sql.split(/ AND /)
+ expect(s).to be_an ActiveRecord::Relation
+ first, last = s.to_sql.split(/ AND /)
expect(first).to match /#{people_name_field} = 'Ernie' OR #{
children_people_name_field} = 'Ernie'/
expect(last).to match /#{people_name_field} = 'Bert' OR #{
@@ -289,7 +298,7 @@ module Ransack
end
it 'returns distinct records when passed distinct: true' do
- search = Search.new(Person,
+ s = Search.new(Person,
g: [
{ m: 'or', comments_body_cont: 'e', articles_comments_body_cont: 'e' }
]
@@ -299,12 +308,12 @@ module Ransack
else
all_or_load, uniq_or_distinct = :load, :distinct
end
- expect(search.result.send(all_or_load).size)
+ expect(s.result.send(all_or_load).size)
.to eq(9000)
- expect(search.result(distinct: true).size)
+ expect(s.result(distinct: true).size)
.to eq(10)
- expect(search.result.send(all_or_load).send(uniq_or_distinct))
- .to eq search.result(distinct: true).send(all_or_load)
+ expect(s.result.send(all_or_load).send(uniq_or_distinct))
+ .to eq s.result(distinct: true).send(all_or_load)
end
private
diff --git a/spec/support/schema.rb b/spec/support/schema.rb
index 796613730..f5c46a66f 100644
--- a/spec/support/schema.rb
+++ b/spec/support/schema.rb
@@ -25,19 +25,30 @@
end
class Person < ActiveRecord::Base
- if ActiveRecord::VERSION::MAJOR == 3
+ if ::ActiveRecord::VERSION::MAJOR == 3
default_scope order('id DESC')
else
default_scope { order(id: :desc) }
end
- belongs_to :parent, :class_name => 'Person', :foreign_key => :parent_id
- has_many :children, :class_name => 'Person', :foreign_key => :parent_id
+ belongs_to :parent, class_name: 'Person', foreign_key: :parent_id
+ has_many :children, class_name: 'Person', foreign_key: :parent_id
has_many :articles
- has_many :published_articles, :class_name => 'Article', :conditions => {published: true}
+ if ActiveRecord::VERSION::MAJOR == 3
+ if RUBY_VERSION >= '2.3'
+ has_many :published_articles, class_name: "Article",
+ conditions: "published = 't'"
+ else
+ has_many :published_articles, class_name: "Article",
+ conditions: { published: true }
+ end
+ else
+ has_many :published_articles, ->{ where(published: true) },
+ class_name: "Article"
+ end
has_many :comments
- has_many :authored_article_comments, :through => :articles,
- :source => :comments, :foreign_key => :person_id
- has_many :notes, :as => :notable
+ has_many :authored_article_comments, through: :articles,
+ source: :comments, foreign_key: :person_id
+ has_many :notes, as: :notable
scope :restricted, lambda { where("restricted = 1") }
scope :active, lambda { where("active = 1") }
@@ -46,7 +57,12 @@ class Person < ActiveRecord::Base
of_age ? where("age >= ?", 18) : where("age < ?", 18)
}
- ransacker :reversed_name, :formatter => proc { |v| v.reverse } do |parent|
+ alias_attribute :full_name, :name
+
+ ransack_alias :term, :name_or_email
+ ransack_alias :daddy, :parent_name
+
+ ransacker :reversed_name, formatter: proc { |v| v.reverse } do |parent|
parent.table[:name]
end
@@ -80,14 +96,15 @@ class Person < ActiveRecord::Base
GROUP BY articles.person_id
)
SQL
+ .squish
Arel.sql(query)
end
def self.ransackable_attributes(auth_object = nil)
if auth_object == :admin
- column_names + _ransackers.keys - ['only_sort']
+ super - ['only_sort']
else
- column_names + _ransackers.keys - ['only_sort', 'only_admin']
+ super - ['only_sort', 'only_admin']
end
end
@@ -98,16 +115,17 @@ def self.ransortable_attributes(auth_object = nil)
column_names + _ransackers.keys - ['only_search', 'only_admin']
end
end
-
end
class Article < ActiveRecord::Base
belongs_to :person
has_many :comments
has_and_belongs_to_many :tags
- has_many :notes, :as => :notable
+ has_many :notes, as: :notable
- if ActiveRecord::VERSION::STRING >= '3.1'
+ alias_attribute :content, :body
+
+ if ::ActiveRecord::VERSION::STRING >= '3.1'
default_scope { where("'default_scope' = 'default_scope'") }
else # Rails 3.0 does not accept a block
default_scope where("'default_scope' = 'default_scope'")
@@ -142,7 +160,7 @@ class Tag < ActiveRecord::Base
end
class Note < ActiveRecord::Base
- belongs_to :notable, :polymorphic => true
+ belongs_to :notable, polymorphic: true
end
module Schema
@@ -150,7 +168,7 @@ def self.create
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define do
- create_table :people, :force => true do |t|
+ create_table :people, force: true do |t|
t.integer :parent_id
t.string :name
t.string :email
@@ -167,7 +185,7 @@ def self.create
t.timestamps null: false
end
- create_table :articles, :force => true do |t|
+ create_table :articles, force: true do |t|
t.integer :person_id
t.string :title
t.text :subject_header
@@ -175,28 +193,28 @@ def self.create
t.boolean :published, default: true
end
- create_table :comments, :force => true do |t|
+ create_table :comments, force: true do |t|
t.integer :article_id
t.integer :person_id
t.text :body
end
- create_table :tags, :force => true do |t|
+ create_table :tags, force: true do |t|
t.string :name
end
- create_table :articles_tags, :force => true, :id => false do |t|
+ create_table :articles_tags, force: true, id: false do |t|
t.integer :article_id
t.integer :tag_id
end
- create_table :notes, :force => true do |t|
+ create_table :notes, force: true do |t|
t.integer :notable_id
t.string :notable_type
t.string :note
end
- create_table :recommendations, :force => true do |t|
+ create_table :recommendations, force: true do |t|
t.integer :person_id
t.integer :target_person_id
t.integer :article_id
@@ -205,22 +223,22 @@ def self.create
10.times do
person = Person.make
- Note.make(:notable => person)
+ Note.make(notable: person)
3.times do
- article = Article.make(:person => person)
+ article = Article.make(person: person)
3.times do
article.tags = [Tag.make, Tag.make, Tag.make]
end
- Note.make(:notable => article)
+ Note.make(notable: article)
10.times do
- Comment.make(:article => article, :person => person)
+ Comment.make(article: article, person: person)
end
end
end
Comment.make(
- :body => 'First post!',
- :article => Article.make(:title => 'Hello, world!')
- )
+ body: 'First post!',
+ article: Article.make(title: 'Hello, world!')
+ )
end
end