diff --git a/CHANGELOG.md b/CHANGELOG.md index d07a287..3bae466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +* Add support for sorting by scopes + PR [973](https://github.com/activerecord-hackery/ransack/pull/973) + + *Diego Borges* + ## Version 1.8.9 - 2018-08-09 * Locked Active Record compatibiity at 5.1.0 diff --git a/README.md b/README.md index 468debb..b0ff364 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,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 diff --git a/lib/ransack/adapters/active_record/context.rb b/lib/ransack/adapters/active_record/context.rb index 76a3008..27afc82 100644 --- a/lib/ransack/adapters/active_record/context.rb +++ b/lib/ransack/adapters/active_record/context.rb @@ -37,9 +37,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 bfa444e..75cc590 100644 --- a/spec/ransack/adapters/active_record/base_spec.rb +++ b/spec/ransack/adapters/active_record/base_spec.rb @@ -576,6 +576,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 48f22ca..2c47319 100644 --- a/spec/support/schema.rb +++ b/spec/support/schema.rb @@ -57,6 +57,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