Skip to content

Commit ab72c33

Browse files
authored
Merge pull request activeadmin#4768 from Fivell/order_clause
order clause refactoring, allow to use custom sql ordering strategies
2 parents 2bb5021 + bcfc9e3 commit ab72c33

File tree

14 files changed

+178
-17
lines changed

14 files changed

+178
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#### Minor
2121

22+
* Support for custom sorting strategies [#4768][] by [@Fivell][]
2223
* Stream CSV downloads as they're generated [#3038][] by [@craigmcnamara][]
2324
* Disable streaming in development for easier debugging [#3535][] by [@seanlinsley][]
2425
* Improved code reloading [#3783][] by [@chancancode][]

docs/3-index-pages/index-as-table.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,24 @@ index do
164164
end
165165
```
166166

167+
## Custom sorting
168+
169+
It is also possible to use database specific expressions and options for sorting by column
170+
171+
```ruby
172+
order_by(:title) do |order_clause|
173+
if order_clause.order == 'desc'
174+
[order_clause.to_sql, 'NULLS LAST'].join(' ')
175+
else
176+
[order_clause.to_sql, 'NULLS FIRST'].join(' ')
177+
end
178+
end
179+
180+
index do
181+
column :title
182+
end
183+
```
184+
167185
## Associated Sorting
168186

169187
You're normally able to sort columns alphabetically, but by default you
@@ -187,6 +205,7 @@ index do
187205
end
188206
```
189207

208+
190209
## Showing and Hiding Columns
191210

192211
The entire index block is rendered within the context of the view, so you can

lib/active_admin/application.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def initialize
107107
# Whether to display 'Current Filters' on search screen
108108
inheritable_setting :current_filters, true
109109

110+
# class to handle ordering
111+
inheritable_setting :order_clause, ActiveAdmin::OrderClause
112+
110113
# Request parameters that are permitted by default
111114
inheritable_setting :permitted_params, [
112115
:utf8, :_method, :authenticity_token, :commit, :id

lib/active_admin/order_clause.rb

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,48 @@
11
module ActiveAdmin
22
class OrderClause
3-
attr_reader :field, :order
3+
attr_reader :field, :order, :active_admin_config
44

5-
def initialize(clause)
5+
def initialize(active_admin_config, clause)
66
clause =~ /^([\w\_\.]+)(->'\w+')?_(desc|asc)$/
77
@column = $1
88
@op = $2
99
@order = $3
10-
10+
@active_admin_config = active_admin_config
1111
@field = [@column, @op].compact.join
1212
end
1313

1414
def valid?
1515
@field.present? && @order.present?
1616
end
1717

18-
def to_sql(active_admin_config)
19-
table = active_admin_config.resource_column_names.include?(@column) ? active_admin_config.resource_table_name : nil
20-
table_column = (@column =~ /\./) ? @column :
21-
[table, active_admin_config.resource_quoted_column_name(@column)].compact.join(".")
18+
def apply(chain)
19+
chain.reorder(sql)
20+
end
2221

22+
def to_sql
2323
[table_column, @op, ' ', @order].compact.join
2424
end
25+
26+
def table
27+
active_admin_config.resource_column_names.include?(@column) ? active_admin_config.resource_table_name : nil
28+
end
29+
30+
def table_column
31+
(@column =~ /\./) ? @column :
32+
[table, active_admin_config.resource_quoted_column_name(@column)].compact.join(".")
33+
end
34+
35+
def sql
36+
custom_sql || to_sql
37+
end
38+
39+
protected
40+
41+
def custom_sql
42+
if active_admin_config.ordering[@column].present?
43+
active_admin_config.ordering[@column].call(self)
44+
end
45+
end
46+
2547
end
2648
end

lib/active_admin/resource.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'active_admin/resource/scope_to'
1111
require 'active_admin/resource/sidebars'
1212
require 'active_admin/resource/belongs_to'
13+
require 'active_admin/resource/ordering'
1314

1415
module ActiveAdmin
1516

@@ -50,6 +51,9 @@ def sort_order
5051
# Set breadcrumb builder
5152
attr_writer :breadcrumb
5253

54+
#Set order clause
55+
attr_writer :order_clause
56+
5357
# Store a reference to the DSL so that we can dereference it during garbage collection.
5458
attr_accessor :dsl
5559

@@ -82,6 +86,7 @@ def initialize(namespace, resource_class, options = {})
8286
include ScopeTo
8387
include Sidebars
8488
include Routes
89+
include Ordering
8590

8691
# The class this resource wraps. If you register the Post model, Resource#resource_class
8792
# will point to the Post class
@@ -144,6 +149,10 @@ def breadcrumb
144149
instance_variable_defined?(:@breadcrumb) ? @breadcrumb : namespace.breadcrumb
145150
end
146151

152+
def order_clause
153+
@order_clause || namespace.order_clause
154+
end
155+
147156
def find_resource(id)
148157
resource = resource_class.public_send *method_for_find(id)
149158
(decorator_class && resource) ? decorator_class.new(resource) : resource

lib/active_admin/resource/ordering.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module ActiveAdmin
2+
class Resource
3+
module Ordering
4+
5+
def ordering
6+
@ordering ||= {}.with_indifferent_access
7+
end
8+
9+
end
10+
end
11+
end

lib/active_admin/resource_controller/data_access.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,10 @@ def apply_authorization_scope(collection)
209209

210210
def apply_sorting(chain)
211211
params[:order] ||= active_admin_config.sort_order
212-
213-
order_clause = OrderClause.new params[:order]
212+
order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order])
214213

215214
if order_clause.valid?
216-
chain.reorder(order_clause.to_sql(active_admin_config))
215+
order_clause.apply(chain)
217216
else
218217
chain # just return the chain
219218
end

lib/active_admin/resource_dsl.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ def initialize(config, resource_class)
88

99
private
1010

11+
# Redefine sort behaviour for column
12+
#
13+
# For example:
14+
#
15+
# # nulls last
16+
# order_by(:age) do |order_clause|
17+
# [order_clause.to_sql, 'NULLS LAST'].join(' ') if order_clause.order == 'desc'
18+
# end
19+
#
20+
# # by last_name but in the case that there is no last name, by first_name.
21+
# order_by(:full_name) do |order_clause|
22+
# ['COALESCE(NULLIF(last_name, ''), first_name), first_name', order_clause.order].join(' ')
23+
# end
24+
#
25+
#
26+
def order_by(column, &block)
27+
config.ordering[column] = block
28+
end
29+
1130
def belongs_to(target, options = {})
1231
config.belongs_to(target, options)
1332
end

lib/active_admin/views/components/table_for.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def build_table_cell(col, resource)
109109
# current_sort[1] #=> asc | desc
110110
def current_sort
111111
@current_sort ||= begin
112-
order_clause = OrderClause.new params[:order]
112+
order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order])
113113

114114
if order_clause.valid?
115115
[order_clause.field, order_clause.order]

lib/generators/active_admin/install/templates/active_admin.rb.erb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,11 @@ ActiveAdmin.setup do |config|
276276
# override the content of the footer here.
277277
#
278278
# config.footer = 'my custom footer text'
279+
280+
# == Sorting
281+
#
282+
# By default ActiveAdmin::OrderClause is used for sorting logic
283+
# You can inherit it with own class and inject it for all resources
284+
#
285+
# config.order_clause = MyOrderClause
279286
end

spec/unit/application_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
expect(application.comments).to eq true
9292
end
9393

94+
it "should have default order clause class" do
95+
expect(application.order_clause).to eq ActiveAdmin::OrderClause
96+
end
97+
9498
describe "authentication settings" do
9599

96100
it "should have no default current_user_method" do

spec/unit/order_clause_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
require 'rails_helper'
22

33
describe ActiveAdmin::OrderClause do
4-
subject { described_class.new clause }
4+
subject { described_class.new(config, clause) }
55

66
let(:application) { ActiveAdmin::Application.new }
7-
let(:namespace) { ActiveAdmin::Namespace.new application, :admin }
8-
let(:config) { ActiveAdmin::Resource.new namespace, Post }
7+
let(:namespace) { ActiveAdmin::Namespace.new application, :admin }
8+
let(:config) { ActiveAdmin::Resource.new namespace, Post }
99

1010
describe 'id_asc (existing column)' do
1111
let(:clause) { 'id_asc' }
@@ -23,7 +23,7 @@
2323
end
2424

2525
specify '#to_sql prepends table name' do
26-
expect(subject.to_sql(config)).to eq '"posts"."id" asc'
26+
expect(subject.to_sql).to eq '"posts"."id" asc'
2727
end
2828
end
2929

@@ -43,7 +43,7 @@
4343
end
4444

4545
specify '#to_sql' do
46-
expect(subject.to_sql(config)).to eq '"virtual_column" asc'
46+
expect(subject.to_sql).to eq '"virtual_column" asc'
4747
end
4848
end
4949

@@ -63,7 +63,7 @@
6363
end
6464

6565
it 'converts to sql' do
66-
expect(subject.to_sql(config)).to eq %Q("hstore_col"->'field' desc)
66+
expect(subject.to_sql).to eq %Q("hstore_col"->'field' desc)
6767
end
6868
end
6969

spec/unit/resource/ordering_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'rails_helper'
2+
3+
module ActiveAdmin
4+
describe Resource, "Ordering" do
5+
describe "#order_by" do
6+
7+
let(:application) { ActiveAdmin::Application.new }
8+
let(:namespace) { ActiveAdmin::Namespace.new application, :admin }
9+
let(:resource_config) { ActiveAdmin::Resource.new namespace, Post }
10+
let(:dsl){ ActiveAdmin::ResourceDSL.new(resource_config, Post) }
11+
12+
it "should register the ordering in the config" do
13+
dsl.run_registration_block do
14+
order_by(:age) do |order_clause|
15+
if order_clause.order == 'desc'
16+
[order_clause.to_sql, 'NULLS LAST'].join(' ')
17+
end
18+
end
19+
end
20+
expect(resource_config.ordering.size).to eq(1)
21+
end
22+
23+
24+
it "should allow to setup custom ordering class" do
25+
MyOrderClause = Class.new(ActiveAdmin::OrderClause)
26+
dsl.run_registration_block do
27+
config.order_clause = MyOrderClause
28+
end
29+
expect(resource_config.order_clause).to eq(MyOrderClause)
30+
expect(application.order_clause).to eq(ActiveAdmin::OrderClause)
31+
32+
end
33+
34+
end
35+
end
36+
end
37+
38+

spec/unit/resource_controller/data_access_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@
6060
end
6161
end
6262

63+
context "custom strategy" do
64+
before do
65+
expect(controller.send(:active_admin_config)).to receive(:ordering).twice.and_return(
66+
{
67+
published_date: proc do |order_clause|
68+
[order_clause.to_sql, 'NULLS LAST'].join(' ') if order_clause.order == 'desc'
69+
end
70+
}.with_indifferent_access
71+
)
72+
end
73+
74+
context "when params applicable" do
75+
let(:params) {{ order: "published_date_desc" }}
76+
it "reorders chain" do
77+
chain = double "ChainObj"
78+
expect(chain).to receive(:reorder).with('"posts"."published_date" desc NULLS LAST').once.and_return(Post.search)
79+
controller.send :apply_sorting, chain
80+
end
81+
end
82+
context "when params not applicable" do
83+
let(:params) {{ order: "published_date_asc" }}
84+
it "reorders chain" do
85+
chain = double "ChainObj"
86+
expect(chain).to receive(:reorder).with('"posts"."published_date" asc').once.and_return(Post.search)
87+
controller.send :apply_sorting, chain
88+
end
89+
end
90+
end
91+
6392
end
6493

6594
describe "scoping" do

0 commit comments

Comments
 (0)