Last active
August 29, 2015 14:16
-
-
Save pboling/f908252811e1e7cc5427 to your computer and use it in GitHub Desktop.
Concern::NestedFilters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Concern | |
module NestedFilters | |
extend ActiveSupport::Concern | |
CACHE_EXPIRATION = 1.days | |
NULL_SORT = "LAST" | |
NestedFilter = Struct.new(:filter_klass, :filter_param_key, :filter_name, :filter_options) | |
NestedFilterOption = Struct.new(:id, :name) | |
included do | |
class_attribute :nested_filter_cache_expiration | |
class_attribute :nested_filter_names | |
class_attribute :nested_filter_sorts | |
class_attribute :nested_filter_null_clause | |
scope :nested_filter_from_params, lambda {|params| where(arel_query_hash_from_params(params)) } | |
end | |
module ClassMethods | |
# filters param can be like any of the following, which are all handled by the Array() cast | |
# 1. "type" => a single value | |
# 2. ["type", "status"] => two filters, no sort preference | |
# 3. { => nested sort with sort preferences (Note: Must be an ordered Hash a la Ruby 1.9+) | |
# "type" => "ASC", | |
# "status" => "DESC", | |
# } | |
# nested_filters_raw will look like: | |
# 1. ["type"] | |
# 2. ["type", "status"] | |
# 3. [["type", "ASC"],["status", "DESC"]] | |
def has_nested_filters(filters, cache_expiration: CACHE_EXPIRATION, null_sort: NULL_SORT) | |
nested_filters_raw = Array(filters).compact | |
self.nested_filter_names = nested_filters_raw.map {|x| Array(x)[0]} | |
self.nested_filter_sorts = nested_filters_raw.map {|x| Array(x)[1] || "ASC"} | |
self.nested_filter_cache_expiration = cache_expiration | |
self.nested_filter_null_clause = null_sort ? " NULLS #{null_sort}" : "" | |
end | |
def nested_filters | |
Rails.cache.fetch(cache_key(base: "nested-filters"), :expires_in => nested_filter_cache_expiration) do | |
self.connection.execute("select DISTINCT #{distinct_sql} from #{table_name} ORDER BY #{order_sql}").values.inject({}) do |hash, n_tuple| | |
build_nested_hash(hash, n_tuple) | |
end | |
end | |
end | |
# nest - an array of values, for example | |
# if the top level filter is "type", | |
# then the first index of nest might be "User" | |
# if the next level filter is "status", | |
# then the second index of nest might be "active" | |
def cache_key(base:, nest: []) | |
"#{self.model_name.param_key}-#{base}#{cache_for_nest(nest: nest)}" | |
end | |
def cache_for_nest(nest: []) | |
nest.any? ? | |
"-#{nest.join("_")}" : | |
"" | |
end | |
def nested_filters_at(nest: []) | |
Rails.cache.fetch(cache_key(base: "nested-filters-at", nest: nest), :expires_in => nested_filter_cache_expiration) do | |
nested_lookup(nested_filters, nest: nest) | |
end | |
end | |
def reverse_nested_filters_at(params: {}) | |
nest_indexes = sort_params_into_nested_indexes(params) | |
reverse_nested_lookup(nested_filters_at, nest: [], nest_indexes: nest_indexes) | |
end | |
private | |
def sort_params_to_tuples(params) | |
params ||= {} | |
params.select {|k,v| nested_filter_names.include?(k) }.sort_by {|key, value| nested_filter_names.index(key) } | |
end | |
def sort_params_into_nested_indexes(params) | |
sort_params_to_tuples(params).to_h.values | |
end | |
def arel_query_hash_from_params(params) | |
query_hash_from_tuples(tuples: sort_params_to_tuples(params)) | |
end | |
def distinct_sql | |
nested_filter_names.compact.join(", ") | |
end | |
def order_sql | |
[nested_filter_names, nested_filter_sorts].transpose.map {|x| x.join(" ")}.join(", ") << nested_filter_null_clause | |
end | |
def build_nested_hash(hash, n_tuple) | |
key = n_tuple. | |
shift. | |
to_s # nil guard | |
if n_tuple.length == 1 | |
hash[key] ||= [] | |
hash[key] << n_tuple[0] | |
else | |
hash[key] ||= {} | |
hash[key].merge!(build_nested_hash(hash[key], n_tuple)) | |
end | |
hash | |
end | |
# returns a NestedFilter struct or nil | |
def nested_lookup(hash, nest: nest, level: 0) | |
# stop the recursion when needed | |
if nest.blank? | |
# When nest has been exhausted | |
if hash.is_a?(Hash) | |
return NestedFilter.new(model_name, model_name.param_key, nested_filter_names[level], nested_filter_options(hash.keys)) | |
else | |
return NestedFilter.new(model_name, model_name.param_key, nested_filter_names[level], nested_filter_options(hash)) | |
end | |
else | |
# Traversed down to a non-hash leaf node, and nest is not blank, then nil is the result of the lookup | |
return nil unless hash.is_a?(Hash) | |
end | |
nested_lookup(hash[nest[0]], nest: nest[1..-1], level: level+1) | |
end | |
# returns an array of NestedFilterOption structs | |
def nested_filter_options(array) | |
array.map.with_index do |value, index| | |
NestedFilterOption.new(index, value) | |
end | |
end | |
def reverse_nested_lookup(nfilter = nested_filters_at, nest: [], nest_indexes: [], level: 0) | |
selected_filter_option_index = nest_indexes[0].to_i | |
return nfilter if nest_indexes.blank? || nfilter.nil? || selected_filter_option_index > (nfilter.filter_options.length - 1) | |
nest << nfilter.filter_options[selected_filter_option_index].name | |
# puts "[reverse_nested_lookup] #{level}: Reverse Nest: #{nest.class} #{nest}, Nest Indexes: #{nest_indexes.class} #{nest_indexes}" | |
reverse_nested_lookup(nested_filters_at(nest: nest), nest: nest, nest_indexes: nest_indexes[1..-1], level: level+1) | |
end | |
def query_hash_from_tuples(nfilter = nested_filters_at, nest: [], tuples: [], query_hash: {}, level: 0) | |
tuple = tuples.shift | |
return query_hash if tuple.blank? || nfilter.nil? | |
selected_filter_option_index = tuple[1].to_i | |
return query_hash if selected_filter_option_index > (nfilter.filter_options.length - 1) | |
query_hash[tuple[0].to_s] = nfilter.filter_options[selected_filter_option_index].name | |
nest << nfilter.filter_options[selected_filter_option_index].name | |
# puts "[reverse_from_tuples] #{level}: tuple: #{tuple}, tuples: #{tuples}, nest: #{nest.inspect}, query_hash: #{query_hash.inspect}, filter_name: #{nfilter.filter_name}" | |
query_hash_from_tuples(nested_filters_at(nest: nest), nest: nest, tuples: tuples, query_hash: query_hash, level: level+1) | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment