diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d4a1e..c567037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +* Add support for sorting by scopes + PR [973](https://github.com/activerecord-hackery/ransack/pull/973) + + *Diego Borges* + ## Version 2.0.1 - 2018-08-18 * Don't return association if table is nil diff --git a/README.md b/README.md index 1a40f2c..ede9051 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ created by [Ernie Miller](http://twitter.com/erniemiller) and developed/maintained for years by [Jon Atack](http://twitter.com/jonatack) and [Ryan Bigg](http://twitter.com/ryanbigg) with the help of a great group of -[contributors](https://github.com/activerecord-hackery/ransack/graphs/contributors). Ransack's logo is designed by [Anıl Kılıç](https://github.com/anilkilic). +[contributors](https://github.com/activerecord-hackery/ransack/graphs/contributors). Ransack's logo is designed by [Anıl Kılıç](https://github.com/anilkilic). While it supports many of the same features as MetaSearch, its underlying implementation differs greatly from MetaSearch, and backwards compatibility is not a design goal. @@ -133,8 +133,8 @@ which are defined in ``` The argument of `f.search_field` has to be in this form: - `attribute_name[_or_attribute_name]..._predicate` - + `attribute_name[_or_attribute_name]..._predicate` + where `[_or_another_attribute_name]...` means any repetition of `_or_` plus the name of the attribute. `cont` (contains) and `start` (starts with) are just two of the available @@ -201,6 +201,23 @@ This example toggles the sort directions of both fields, by default initially sorting the `last_name` field by ascending order, and the `first_name` field by descending order. +In the case that you wish to sort by some complex value, such as the result +of a SQL function, you may do so using scopes. In your model, define scopes +whose names line up with the name of the virtual field you wish to sort by, +as so: + +```ruby +class Person < ActiveRecord::Base + scope :sort_by_reverse_name_asc, lambda { order("REVERSE(name) ASC") } + scope :sort_by_reverse_name_desc, lambda { order("REVERSE(name) DESC") } +... +``` + +and you can then sort by this virtual field: + +```erb +<%= sort_link(@q, :reverse_name) %> +``` The sort link order indicator arrows may be globally customized by setting a `custom_arrows` option in an initializer file like @@ -727,7 +744,7 @@ Ransack.configure do |c| end ``` -To turn this off on a per-scope basis Ransack adds the following method to +To turn this off on a per-scope basis Ransack adds the following method to `ActiveRecord::Base` that you can redefine to selectively override sanitization: `ransackable_scopes_skip_sanitize_args` diff --git a/lib/ransack/adapters/active_record/context.rb b/lib/ransack/adapters/active_record/context.rb index f91ff8c..201dc70 100644 --- a/lib/ransack/adapters/active_record/context.rb +++ b/lib/ransack/adapters/active_record/context.rb @@ -31,9 +31,29 @@ module Ransack def evaluate(search, opts = {}) viz = Visitor.new relation = @object.where(viz.accept(search.base)) + if search.sorts.any? - relation = relation.except(:order).reorder(viz.accept(search.sorts)) + relation = relation.except(:order) + # Rather than applying all of the search's sorts in one fell swoop, + # as the original implementation does, we apply one at a time. + # + # If the sort (returned by the Visitor above) is a symbol, we know + # that it represents a scope on the model and we can apply that + # scope. + # + # Otherwise, we fall back to the applying the sort with the "order" + # method as the original implementation did. Actually the original + # implementation used "reorder," which was overkill since we already + # have a clean slate after "relation.except(:order)" above. + viz.accept(search.sorts).each do |scope_or_sort| + if scope_or_sort.is_a?(Symbol) + relation = relation.send(scope_or_sort) + else + relation = relation.order(scope_or_sort) + end + end end + opts[:distinct] ? relation.distinct : relation end diff --git a/lib/ransack/adapters/active_record/ransack/visitor.rb b/lib/ransack/adapters/active_record/ransack/visitor.rb index 549ee61..e06bc4b 100644 --- a/lib/ransack/adapters/active_record/ransack/visitor.rb +++ b/lib/ransack/adapters/active_record/ransack/visitor.rb @@ -21,11 +21,15 @@ module Ransack end def visit_Ransack_Nodes_Sort(object) - return unless object.valid? - if object.attr.is_a?(Arel::Attributes::Attribute) - object.attr.send(object.dir) + if object.valid? + if object.attr.is_a?(Arel::Attributes::Attribute) + object.attr.send(object.dir) + else + ordered(object) + end else - ordered(object) + scope_name = :"sort_by_#{object.name}_#{object.dir}" + scope_name if object.context.object.respond_to?(scope_name) end end diff --git a/spec/ransack/adapters/active_record/base_spec.rb b/spec/ransack/adapters/active_record/base_spec.rb index eae3d31..90c32bd 100644 --- a/spec/ransack/adapters/active_record/base_spec.rb +++ b/spec/ransack/adapters/active_record/base_spec.rb @@ -605,6 +605,13 @@ module Ransack expect(search.result.to_sql).to match /ORDER BY .* ASC/ end end + + context 'sorting by a scope' do + it 'applies the correct scope' do + search = Person.search(sorts: ['reverse_name asc']) + expect(search.result.to_sql).to include("ORDER BY REVERSE(name) ASC") + end + end end describe '#ransackable_attributes' do diff --git a/spec/support/schema.rb b/spec/support/schema.rb index 11647d4..b83a31b 100644 --- a/spec/support/schema.rb +++ b/spec/support/schema.rb @@ -43,6 +43,9 @@ class Person < ActiveRecord::Base of_age ? where("age >= ?", 18) : where("age < ?", 18) } + scope :sort_by_reverse_name_asc, lambda { order("REVERSE(name) ASC") } + scope :sort_by_reverse_name_desc, lambda { order("REVERSE(name) DESC") } + alias_attribute :full_name, :name ransack_alias :term, :name_or_email