Created
October 9, 2025 16:20
-
-
Save andynu/378009567631982cc351b92fbba047b2 to your computer and use it in GitHub Desktop.
example concerns
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
require 'active_support/concern' | |
# The {FiltersConcern} provides a <tt>filter_params</tt> scope which takes a hash <tt>{ col1: term, col2: term }</tt> | |
# | |
# see app/assets/javascripts/filterable.js | |
# | |
# == type sensitive filtering | |
# * strings - fuzzy search of %term% | |
# * dates - | |
# * July - => month | |
# * July 2025 => month/year | |
# * #### => year, | |
# * ##/#### => month/year, | |
# * ##/##/#### => full date | |
# * other - exact term matching | |
# | |
# == composite columns/custom | |
# For composite columns with filter params like { "col_a,col_b" => term } it will attempt to call a | |
# custom scope. | |
# | |
# scope :filter_col_a_col_b, (term) -> { ... } | |
# | |
# Generally <tt>param[filters][custom]</tt> => <tt>scope :filter_custom</tt> | |
# | |
# With custom getting scrubbed of punctuation with underscores and squeezed. | |
# | |
# == Usage | |
# | |
# === Model | |
# | |
# class Whatever < ApplicationRecord | |
# include FiltersConcern | |
# | |
# === Form Template | |
# | |
# = form_for ... do |f| | |
# = text_field_tag 'filters[col1]' | |
# = text_field_tag 'filters[col2]' | |
# ... | |
# | |
# === Controller | |
# | |
# def index | |
# @records = Whatever.all | |
# @records = @records.filter_params(params[:filters]) | |
# ... | |
# | |
# == Generators | |
# | |
# This is built into our scaffold generator. see lib/templates | |
# | |
module FiltersConcern | |
extend ActiveSupport::Concern | |
included do | |
# filters is a hash of { column => search_value, ... } | |
# Strings fuzzy search. Everything else is an exact match. | |
scope :filter_params, ->(filters, table_prefix: nil) { | |
table_prefix = "#{table_prefix}." unless table_prefix.nil? | |
query = all | |
if filters.present? | |
filters.each_pair do |column, value| | |
if value.blank? | |
logger.debug "Filtering: #{column} => ALL (value='#{value}')" | |
next | |
end | |
custom_filter_scope = :"filter_#{column.gsub(/[^[:alnum:]]/, '_').squeeze('_')}" | |
if self.respond_to? custom_filter_scope | |
logger.debug("Filtering: using custom scope #{custom_filter_scope}('#{value}')") | |
query = query.send(custom_filter_scope, value) | |
elsif column.in?(attribute_names) | |
col_type = query.type_for_attribute(column).type | |
logger.debug "Filtering: #{column}=#{value} (type: #{col_type})" | |
case col_type | |
when :string | |
query = query.where("#{table_prefix}#{column} like ?", "%#{value}%") | |
when :date, :datetime | |
year_i = nil | |
month_i = nil | |
day_i = nil | |
if value =~ /^(January|February|March|April|May|June|July|August|September|October|November|December)(\s+\d{4})?$/i | |
logger.debug 'FiltersConcern month word detected' | |
month_capture = value.match(/^(January|February|March|April|May|June|July|August|September|October|November|December)/i)[0] | |
month_i = Chronic.parse(month_capture+" 1st 1970").month | |
year_i = value.match(/\d{4}\b/)&.try(:"[]", 0)&.to_i | |
elsif value =~ /^\d{4}$/ | |
logger.debug 'FiltersConcern year filter' | |
year_i = value.to_i | |
elsif value =~ /^\d{1,2}\/\d{4}$/ | |
logger.debug 'FiltersConcern month/year filter' | |
month_i, year_i = value.split('/').map(&:to_i) | |
elsif value =~ /^\d{1,2}\/\d{1,2}\/\d{2,4}$/ | |
logger.debug 'FiltersConcern month/day/year filter' | |
month_i, day_i, year_i = value.split('/').map(&:to_i) | |
elsif value =~ /^\d{1,2}$/ && value.to_i <= 12 | |
logger.debug 'FiltersConcern month filter' | |
month_i = value.to_i | |
else | |
logger.debug "Unknown date format: '#{value}'" | |
end | |
if year_i || month_i || day_i | |
if defined?(Mysql2) | |
query = query.where("year(#{table_prefix}#{column}) = ?", year_i) if year_i.present? | |
query = query.where("month(#{table_prefix}#{column}) = ?", month_i) if month_i.present? | |
query = query.where("day(#{table_prefix}#{column}) = ?", day_i) if day_i.present? | |
elsif defined?(PG) | |
query = query.where("extract(year from #{table_prefix}#{column}) = ?", year_i) if year_i.present? | |
query = query.where("extract(month from #{table_prefix}#{column}) = ?", month_i) if month_i.present? | |
query = query.where("extract(day from #{table_prefix}#{column}) = ?", day_i) if day_i.present? | |
else | |
logger.warn 'FiltersConcern filter_params on date column with unsupported database type' | |
end | |
end | |
else | |
query = query.where(column => value) | |
end | |
logger.debug "FiltersConcern query: #{query.to_sql}" | |
else | |
logger.warn "Could not filter composite column #{column}. To handle this add a custom scope :#{custom_filter_scope}, ->(term){ ... }" | |
end | |
end | |
else | |
logger.debug 'no filters' | |
query | |
end | |
#logger.warn query.inspect | |
query | |
} | |
end | |
end |
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
require 'active_support/concern' | |
# This version of SortableConcern requires at least v1.1.0 of sortable.js | |
# | |
# Provides a <tt>sortable</tt> scope that takes the params object. | |
# | |
# Dependency: | |
# * wi_helper gem (provides query_params_url) | |
# | |
# Expects params: | |
# * sortby - must be one of the attribute_names of the ActiveRecord class. | |
# * sortord - 'asc' || 'desc' | |
# * sortrel - may specify a valid relation for the ActiveRecord class to use for the sortby attribute | |
# | |
# == Usage | |
# === Model | |
# class Whatever < ApplicationRecord | |
# include SortableConcern | |
# | |
# === Controller | |
# def index | |
# @records = Whatever.all | |
# @records = @records.sortable(params) | |
# | |
# === View | |
# | |
# Works in conjunction with app/assets/javascripts/sortable.js | |
# | |
# %table.ui.table.sortable{ data: { sorturl: query_params_url(sortby: nil, sortdir: nil, sortrel: nil, page: 1) } } | |
# %thead | |
# %th{ sortby: 'col1' } Col1 | |
# %th{ sortby: 'col2' } Col2 | |
# %th{ sortby: 'col3', sortrel: 'related' } Col3 | |
# | |
# sortable.js accepts clicks on the titles reloading using <tt>sorturl</tt> and the column's <tt>sortby</tt> alternating between sortord asc & desc. | |
# If sortrel is specified and valid, its table name will be added to the query via .joins() and used for ordering. | |
# | |
# The use of <tt>query_params_url</tt> preserves other query params, so this works along with the {FiltersConcern} | |
# | |
# == Generators | |
# | |
# This is built into our scaffold generator. see lib/templates | |
module SortableConcern | |
extend ActiveSupport::Concern | |
included do | |
scope :sortable, ->(params) { | |
sortby, sortdir, sortrel = params.values_at(:sortby, :sortdir, :sortrel) | |
sortdir = sortdir == 'desc' ? 'desc' : 'asc' | |
if sortrel.blank? && sortby.in?(attribute_names) | |
logger.info " sortable: #{self.klass.table_name}.#{sortby} #{sortdir}" | |
reorder(Arel.sql("#{self.klass.table_name}.#{sortby} #{sortdir}")) | |
elsif self.klass.reflections.key?(sortrel) && sortby.in?(self.klass.reflections[sortrel].klass.attribute_names) | |
joins(sortrel.to_sym).reorder(Arel.sql("#{self.klass.reflections[sortrel].table_name}.#{sortby} #{sortdir}")) | |
end | |
} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment