#-- # Copyright (c) 2011 Michael Berkovich # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ module WillFilter class Filter < ActiveRecord::Base set_table_name :wf_filters serialize :data before_save :prepare_save after_find :process_find ############################################################################# # Basics ############################################################################# def initialize(model_class) super() self.model_class_name = model_class.to_s end def dup super.tap {|ii| ii.conditions = self.conditions.dup} end def prepare_save self.data = serialize_to_params self.type = self.class.name end def process_find @errors = {} deserialize_from_params(self.data) end ############################################################################# # Defaults ############################################################################# def show_export_options? WillFilter::Config.exporting_enabled? end def show_save_options? WillFilter::Config.saving_enabled? end def match @match ||= :all end def key @key ||= '' end def errors @errors ||= {} end def format @format ||= :html end def fields @fields ||= [] end ############################################################################# # a list of indexed fields where at least one of them has to be in a query # otherwise the filter may hang the database ############################################################################# def required_condition_keys [] end def model_class return nil unless model_class_name @model_class ||= model_class_name.constantize end def table_name model_class.table_name end def key=(new_key) @key = new_key end def match=(new_match) @match = new_match end ############################################################################# # Inner Joins come in a form of # [[joining_model_name, column_name], [joining_model_name, column_name]] ############################################################################# def inner_joins [] end def model_columns model_class.columns end def model_column_keys model_columns.collect{|col| col.name.to_sym} end def contains_column?(key) model_column_keys.index(key) != nil end def definition @definition ||= begin defs = {} model_columns.each do |col| defs[col.name.to_sym] = default_condition_definition_for(col.name, col.sql_type) end inner_joins.each do |inner_join| join_class = inner_join.first.to_s.camelcase.constantize join_class.columns.each do |col| defs[:"#{join_class.to_s.underscore}.#{col.name.to_sym}"] = default_condition_definition_for(col.name, col.sql_type) end end defs end end def container_by_sql_type(type) raise WillFilter::FilterException.new("Unsupported data type #{type}") unless WillFilter::Config.data_types[type] WillFilter::Config.data_types[type] end def default_condition_definition_for(name, sql_data_type) type = sql_data_type.split(" ").first.split("(").first.downcase containers = container_by_sql_type(type) operators = {} containers.each do |c| raise WillFilter::FilterException.new("Unsupported container implementation for #{c}") unless WillFilter::Config.containers[c] container_klass = WillFilter::Config.containers[c].constantize container_klass.operators.each do |o| operators[o] = c end end if name == "id" operators[:is_filtered_by] = :filter_list elsif "_id" == name[-3..-1] begin name[0..-4].camelcase.constantize operators[:is_filtered_by] = :filter_list rescue end end operators end def sorted_operators(opers) (WillFilter::Config.operator_order & opers.keys.collect{|o| o.to_s}) end def first_sorted_operator(opers) sorted_operators(opers).first.to_sym end def default_order 'id' end def order @order ||= default_order end def default_order_type 'desc' end def order_type @order_type ||= default_order_type end def order_clause "#{order} #{order_type}" end def column_sorted?(key) key.to_s == order end def default_per_page 30 end def per_page @per_page ||= default_per_page end def page @page ||= 1 end def default_per_page_options [10, 20, 30, 40, 50, 100] end def per_page_options @per_page_options ||= default_per_page_options.collect{ |n| [n.to_s, n.to_s] } end def match_options [["all", "all"], ["any", "any"]] end def order_type_options [["desc", "desc"], ["asc", "asc"]] end ############################################################################# # Can be overloaded for custom titles ############################################################################# def condition_title_for(key) title = key.to_s.gsub(".", ": ").gsub("_", " ") title.split(" ").collect{|part| part.capitalize}.join(" ") end def condition_options @condition_options ||= begin opts = [] definition.keys.each do |cond| opts << [condition_title_for(cond), cond.to_s] end opts.sort_by{ |i| i[0] } end end def operator_options_for(condition_key) condition_key = condition_key.to_sym if condition_key.is_a?(String) opers = definition[condition_key] raise WillFilter::FilterException.new("Invalid condition #{condition_key} for filter #{self.class.name}") unless opers sorted_operators(opers).collect{|o| [o.to_s.gsub('_', ' '), o]} end # called by the list container, should be overloaded in a subclass def value_options_for(condition_key) [] end def container_for(condition_key, operator_key) condition_key = condition_key.to_sym if condition_key.is_a?(String) opers = definition[condition_key] raise WillFilter::FilterException.new("Invalid condition #{condition_key} for filter #{self.class.name}") unless opers oper = opers[operator_key] # if invalid operator_key was passed, use first operator oper = opers[first_sorted_operator(opers)] unless oper oper end def add_condition(condition_key, operator_key, values = []) add_condition_at(size, condition_key, operator_key, values) end def valid_operator?(condition_key, operator_key) condition_key = condition_key.to_sym if condition_key.is_a?(String) opers = definition[condition_key] return false unless opers opers[operator_key]!=nil end def add_condition_at(index, condition_key, operator_key, values = []) values = [values] unless values.instance_of?(Array) values = values.collect{|v| v.to_s} condition_key = condition_key.to_sym if condition_key.is_a?(String) unless valid_operator?(condition_key, operator_key) opers = definition[condition_key] operator_key = first_sorted_operator(opers) end condition = WillFilter::FilterCondition.new(self, condition_key, operator_key, container_for(condition_key, operator_key), values) @conditions.insert(index, condition) end ############################################################################# # options always go in [NAME, KEY] format ############################################################################# def default_condition_key condition_options.first.last end ############################################################################# # options always go in [NAME, KEY] format ############################################################################# def default_operator_key(condition_key) operator_options_for(condition_key).first.last end def conditions=(new_conditions) @conditions = new_conditions end def conditions @conditions ||= [] end def condition_at(index) conditions[index] end def condition_by_key(key) conditions.each do |c| return c if c.key==key end nil end def size conditions.size end def add_default_condition_at(index) add_condition_at(index, default_condition_key, default_operator_key(default_condition_key)) end def remove_condition_at(index) conditions.delete_at(index) end def remove_all @conditions = [] end ############################################################################# # Serialization ############################################################################# def serialize_to_params(merge_params = {}) params = {} params[:wf_type] = self.class.name params[:wf_match] = match params[:wf_model] = model_class_name params[:wf_order] = order params[:wf_order_type] = order_type params[:wf_per_page] = per_page params[:wf_export_fields] = fields.join(',') params[:wf_export_format] = format 0.upto(size - 1) do |index| condition = condition_at(index) condition.serialize_to_params(params, index) end params.merge(merge_params) end ############################################################################# # allows to create a filter from params only ############################################################################# def self.deserialize_from_params(params) params = HashWithIndifferentAccess.new(params) unless params.is_a?(HashWithIndifferentAccess) params[:wf_type] = self.name unless params[:wf_type] params[:wf_type].constantize.new(params[:wf_model]).deserialize_from_params(params) end def deserialize_from_params(params) params = HashWithIndifferentAccess.new(params) unless params.is_a?(HashWithIndifferentAccess) @conditions = [] @match = params[:wf_match] || :all @key = params[:wf_key] || self.id.to_s self.model_class_name = params[:wf_model] if params[:wf_model] @per_page = params[:wf_per_page] || default_per_page @page = params[:page] || 1 @order_type = params[:wf_order_type] || default_order_type @order = params[:wf_order] || default_order self.id = params[:wf_id].to_i unless params[:wf_id].blank? self.name = params[:wf_name] unless params[:wf_name].blank? @fields = [] unless params[:wf_export_fields].blank? params[:wf_export_fields].split(",").each do |fld| @fields << fld.to_sym end end if params[:wf_export_format].blank? @format = :html else @format = params[:wf_export_format].to_sym end i = 0 while params["wf_c#{i}"] do conditon_key = params["wf_c#{i}"] operator_key = params["wf_o#{i}"] values = [] j = 0 while params["wf_v#{i}_#{j}"] do values << params["wf_v#{i}_#{j}"] j += 1 end i += 1 add_condition(conditon_key, operator_key.to_sym, values) end if params[:wf_submitted] == 'true' validate! end return self end ############################################################################# # Validations ############################################################################# def errors? (@errors and @errors.size > 0) end def empty? size == 0 end def has_condition?(key) condition_by_key(key) != nil end def valid_format? WillFilter::Config.default_export_formats.include?(format.to_s) end def required_conditions_met? return true if required_condition_keys.blank? sconditions = conditions.collect{|c| c.key.to_s} rconditions = required_condition_keys.collect{|c| c.to_s} not (sconditions & rconditions).empty? end def validate! @errors = {} 0.upto(size - 1) do |index| condition = condition_at(index) err = condition.validate @errors[index] = err if err end unless required_conditions_met? @errors[:filter] = "Filter must contain at least one of the following conditions: #{required_condition_keys.join(", ")}" end errors? end ############################################################################# # SQL Conditions ############################################################################# def sql_conditions @sql_conditions ||= begin if errors? all_sql_conditions = [" 1 = 2 "] else all_sql_conditions = [""] 0.upto(size - 1) do |index| condition = condition_at(index) sql_condition = condition.container.sql_condition unless sql_condition raise WillFilter::FilterException.new("Unsupported operator #{condition.operator_key} for container #{condition.container.class.name}") end if all_sql_conditions[0].size > 0 all_sql_conditions[0] << ( match.to_sym == :all ? " AND " : " OR ") end all_sql_conditions[0] << sql_condition[0] sql_condition[1..-1].each do |c| all_sql_conditions << c end end end all_sql_conditions end end def debug_conditions(conds) all_conditions = [] conds.each_with_index do |c, i| cond = "" if i == 0 cond << "\"#{c}\"" else cond << "
   #{i}) " if c.is_a?(Array) cond << "[" cond << (c.collect{|v| "\"#{v.strip}\""}.join(", ")) cond << "]" elsif c.is_a?(Date) cond << "\"#{c.strftime("%Y-%m-%d")}\"" elsif c.is_a?(Time) cond << "\"#{c.strftime("%Y-%m-%d %H:%M:%S")}\"" elsif c.is_a?(Integer) cond << c.to_s else cond << "\"#{c}\"" end end all_conditions << cond end all_conditions end def debug_sql_conditions debug_conditions(sql_conditions) end ############################################################################# # Saved Filters ############################################################################# def saved_filters(include_default = true) @saved_filters ||= begin filters = [] if include_default filters = default_filters if (filters.size > 0) filters.insert(0, ["-- Select Default Filter --", "-1"]) end end if include_default conditions = ["type = ? and model_class_name = ?", self.class.name, self.model_class_name] else conditions = ["model_class_name = ?", self.model_class_name] end if WillFilter::Config.user_filters_enabled? conditions[0] << " and user_id = ? " if WillFilter::Config.current_user and WillFilter::Config.current_user.id conditions << WillFilter::Config.current_user.id else conditions << "0" end end user_filters = WillFilter::Filter.find(:all, :conditions => conditions) if user_filters.size > 0 filters << ["-- Select Saved Filter --", "-2"] if include_default user_filters.each do |filter| filters << [filter.name, filter.id.to_s] end end filters end end ############################################################################# # overload this method if you don't want to allow empty filters ############################################################################# def default_filter_if_empty nil end def handle_empty_filter! return unless empty? return if default_filter_if_empty.nil? load_filter!(default_filter_if_empty) end def default_filters [] end def default_filter_conditions(key) [] end def load_default_filter(key) default_conditions = default_filter_conditions(key) return if default_conditions.nil? or default_conditions.empty? unless default_conditions.first.is_a?(Array) add_condition(*default_conditions) return end default_conditions.each do |default_condition| add_condition(*default_condition) end end def reset! remove_all @sql_conditions = nil @results = nil end def load_filter!(key_or_id) reset! @key = key_or_id.to_s load_default_filter(key) return self unless empty? filter = WillFilter::Filter.find_by_id(key_or_id.to_i) raise WillFilter::FilterException.new("Invalid filter key #{key_or_id.to_s}") if filter.nil? filter end ############################################################################# # Export Filter Data ############################################################################# def export_formats formats = [] formats << ["-- Generic Formats --", -1] WillFilter::Config.default_export_formats.each do |frmt| formats << [frmt, frmt] end if custom_formats.size > 0 formats << ["-- Custom Formats --", -2] custom_formats.each do |frmt| formats << frmt end end formats end def custom_format? custom_formats.each do |frmt| return true if frmt[1].to_sym == format end false end def custom_formats [] end def process_custom_format "" end def joins return nil if inner_joins.empty? inner_joins.collect do |inner_join| join_table_name = inner_join.first.to_s.camelcase.constantize.table_name join_on_field = inner_join.last.to_s "INNER JOIN #{join_table_name} ON #{join_table_name}.id = #{table_name}.#{join_on_field}" end end def results @results ||= begin handle_empty_filter! recs = model_class.paginate(:order => order_clause, :page => page, :per_page => per_page, :conditions => sql_conditions, :joins => joins) recs.wf_filter = self recs end end # sums up the column for the given conditions def sum(column_name) model_class.sum(column_name, :conditions => sql_conditions) end end end