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