Recently, I had a requirement wherein I need to have sorting on the keys of a json column data. We are using postgres 9.4 and the
schema of the json data is static for all the records. The keys needed to be displayed as columns with sorting enabled against them.
Below is the way I implemented it. Also, on digging into ActiveAdmin, I learned some interesting implementation details of the library.
Lets take an example. We have a model named Box which stores (length, width & height) as json in a column named dimensions.
In ActiveAdmin, columns can be displayed for non-table fields. All you have to do is project them in the query. In the below example,
length, breadth and height are not columns of boxes but have been projected in the select query.
1234567891011121314
ActiveAdmin.registerBoxdocontrollerdodefscoped_collectionBox.select("*, dimensions ->> 'length' as length, dimensions ->> 'breadth' as breadth, dimensions ->> 'height' as height")endendindexdocolumn:length,label:'Length',sortable:"dimensions->>'length'"column:breadth,label:'Breadth',sortable:"dimensions->>'breadth'"column:height,label:'Height',sortable:"dimensions->>'height'"column:volume,label:'Volume',sortable:trueendend
When defining the columns to be displayed, we pass the query by which the json key would be accessed when sorting is applied.
By default ActiveAdmin appends _asc and _desc to the table columns when sorting. Here, we pass the query overriding the default column name.
ActiveAdmin has a method named apply_sorting
which check if the order is valid or not and calls the ordering query.
12345678910111213141516171819202122232425
# When sorting the breadth in descending order, the params sent to the controller are as below.# {"order"=>"dimensions->>'length'_desc", "controller"=>"admin/boxes", "action"=>"index"}defapply_sorting(chain)params[:order]||=active_admin_config.sort_order#default order for sorting which is 'id_desc'orders=[]params[:order].split('_and_').eachdo|fragment|order_clause=ActiveAdmin::OrderClause.new(fragment)#dimensions->>'breadth'_desc# <ActiveAdmin::OrderClause:0x007fad6dfe @column="dimensions->>'length'", @field="dimensions->>'length'", # @op=nil, @order="desc">iforder_clause.valid?#check if field.present? and order.present?orders<<order_clause.to_sql(active_admin_config)endendiforders.empty?chainelsechain.reorder(orders.shift).order(orders)# executes SELECT *, dimensions ->> 'length' as length, dimensions ->> 'breadth' as breadth, # dimensions ->> 'height' as height # FROM "boxes" ORDER BY "boxes".dimensions->>'length' desc;endend
The last change that we need is to rewrite the
ActiveAdmin::OrderClause. The OrderClause parses the field and order
string(say, volume_desc or dimensions->>‘length’_desc) and splits it into the field name, operation and the order thats to be applied.
# Original codemoduleActiveAdminclassOrderClauseattr_reader:field,:orderdefinitialize(clause)clause=~/^([\w\_\.]+)(->'\w+')?_(desc|asc)$/@column=$1@op=$2@order=$3@field=[@column,@op].compact.joinenddefvalid?@field.present?&&@order.present?enddefto_sql(active_admin_config)table=active_admin_config.resource_column_names.include?(@column)?active_admin_config.resource_table_name:niltable_column=(@column=~/\./)?@column:[table,active_admin_config.resource_quoted_column_name(@column)].compact.join(".")[table_column,@op,' ',@order].compact.joinendendend# Updated code in config/initializers/hacked_order_clause.rbmoduleActiveAdminclassOrderClauseattr_reader:field,:orderdefinitialize(clause)clause=~/^([\w\_\.]+)(->'\w+')?_(desc|asc)$|^([\w\_\.]+->>'[\w\_]+')(->'\w+')?_(desc|asc)$/@column=$1||$4@op=$2||$5@order=$3||$6@field=[@column,@op].compact.joinenddefvalid?@field.present?&&@order.present?enddefto_sql(active_admin_config)table=column_in_table?(active_admin_config.resource_column_names,@column)?active_admin_config.resource_table_name:niltable_column=(@column=~/\./)?@column:[table,json_column?(@column)?@column:active_admin_config.resource_quoted_column_name(@column)].compact.join(".")[table_column,@op,' ',@order].compact.joinendprivatedefjson_column?(column)column.include?('->>')enddefcolumn_in_table?(names,column)column=json_column?(column)?column.split('->>')[0].strip:columnnames.include?(column)endendend
Working source code can be found here. Hope it was interesting.