lib/active_scaffold/finder.rb in active_scaffold-3.4.43 vs lib/active_scaffold/finder.rb in active_scaffold-3.5.0

- old
+ new

@@ -3,51 +3,105 @@ def self.like_operator @@like_operator ||= ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? 'ILIKE' : 'LIKE' end module ClassMethods + def self.extended(klass) + return unless klass.active_scaffold_config + if klass.active_scaffold_config.active_record? + klass.extend ActiveRecord + elsif klass.active_scaffold_config.mongoid? + klass.extend Mongoid + end + end + # Takes a collection of search terms (the tokens) and creates SQL that # searches all specified ActiveScaffold columns. A row will match if each # token is found in at least one of the columns. - def create_conditions_for_columns(tokens, columns, text_search = :full) + def conditions_for_columns(tokens, columns, text_search = :full) # if there aren't any columns, then just return a nil condition return unless columns.any? - like_pattern = like_pattern(text_search) tokens = [tokens] if tokens.is_a? String + tokens = type_casted_tokens(tokens, columns, like_pattern(text_search)) + create_conditions_for_columns(tokens, columns) + end - where_clauses = [] - columns.each do |column| - Array(column.search_sql).each do |search_sql| - where_clauses << "#{search_sql} #{column.text? ? ActiveScaffold::Finder.like_operator : '='} ?" + def type_casted_tokens(tokens, columns, like_pattern) + tokens.map do |value| + columns.each_with_object({}) do |column, column_tokens| + column_tokens[column.name] = column.text? ? like_pattern.sub('?', value) : ActiveScaffold::Core.column_type_cast(value, column.column) end end - phrase = where_clauses.join(' OR ') + end - tokens.collect do |value| - columns.each_with_object([phrase]) do |column, condition| - Array(column.search_sql).size.times do - condition.push(column.text? ? like_pattern.sub('?', value) : ActiveScaffold::Core.column_type_cast(value, column.column)) + module ActiveRecord + def create_conditions_for_columns(tokens, columns) + where_clauses = [] + columns.each do |column| + column.search_sql.each do |search_sql| + where_clauses << "#{search_sql} #{column.text? ? ActiveScaffold::Finder.like_operator : '='} ?" end end + phrase = where_clauses.join(' OR ') + + tokens.map do |columns_token| + columns.each_with_object([phrase]) do |column, condition| + condition.concat([columns_token[column.name]] * column.search_sql.size) + end + end end + + def like_pattern(text_search) + case text_search + when :full then '%?%' + when :start then '?%' + when :end then '%?' + else '?' + end + end end + module Mongoid + def create_conditions_for_columns(tokens, columns) + conditions = tokens.map do |columns_token| + token_conditions = columns.map do |column| + value = columns_token[column.name] + value = /#{value}/ if column.text? + column.search_sql.map do |search_sql| + {search_sql => value} + end + end.flatten + active_scaffold_config.model.or(token_conditions).selector + end + [active_scaffold_config.model.and(conditions).selector] + end + + def like_pattern(text_search) + case text_search + when :full then '?' + when :start then '^?' + when :end then '?$' + else '^?$' + end + end + end + # Generates an SQL condition for the given ActiveScaffold column based on # that column's database type (or form_ui ... for virtual columns?). # TODO: this should reside on the column, not the controller def condition_for_column(column, value, text_search = :full) like_pattern = like_pattern(text_search) value = value.with_indifferent_access if value.is_a? Hash - if self.respond_to?("condition_for_#{column.name}_column") + if respond_to?("condition_for_#{column.name}_column") return send("condition_for_#{column.name}_column", column, value, like_pattern) end - return unless column && column.search_sql && !value.blank? - search_ui = column.search_ui || column.column.try(:type) + return unless column && column.search_sql && value.present? + search_ui = column.search_ui || column.column_type begin sql, *values = - if search_ui && self.respond_to?("condition_for_#{search_ui}_type") + if search_ui && respond_to?("condition_for_#{search_ui}_type") send("condition_for_#{search_ui}_type", column, value, like_pattern) else if column.search_sql.instance_of? Proc column.search_sql.call(value) else @@ -58,11 +112,11 @@ conditions = [column.search_sql.collect { |search_sql| sql % {:search_sql => search_sql} }.join(' OR ')] conditions += values * column.search_sql.size if values.present? conditions rescue StandardError => e - logger.error "#{e.class.name}: #{e.message} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{name}" + Rails.logger.error "#{e.class.name}: #{e.message} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{name}" raise e end end def condition_for_search_ui(column, value, like_pattern, search_ui) @@ -74,11 +128,12 @@ when :string, :range condition_for_range(column, value, like_pattern) when :date, :time, :datetime, :timestamp condition_for_datetime(column, value) when :select, :multi_select, :country, :usa_state, :chosen, :multi_chosen - ['%{search_sql} in (?)', Array(value)] + values = Array(value).select(&:present?) + ['%{search_sql} in (?)', values] if values.present? else if column.text? ["%{search_sql} #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)] else ['%{search_sql} = ?', ActiveScaffold::Core.column_type_cast(value, column.column)] @@ -118,78 +173,106 @@ elsif ActiveScaffold::Finder::NUMERIC_COMPARATORS.include?(value[:opt]) ["%{search_sql} #{value[:opt]} ?", value[:from]] end end - def translate_days_and_months(value, format) + def tables_for_translating_days_and_months(format) keys = { '%A' => 'date.day_names', '%a' => 'date.abbr_day_names', '%B' => 'date.month_names', '%b' => 'date.abbr_month_names' } - keys.each do |f, k| - if format.include? f - table = Hash[I18n.t(k).compact.zip(I18n.t(k, :locale => :en).compact)] - value.gsub!(Regexp.union(table.keys)) { |s| table[s] } - end + key_index = keys.keys.map { |key| [key, format.index(key)] }.to_h + keys.select! { |k, _| key_index[k] } + keys.sort_by { |k, _| key_index[k] }.map do |_, k| + I18n.t(k).compact.zip(I18n.t(k, :locale => :en).compact).to_h end - value end - def condition_value_for_datetime(column, value, conversion = :to_time) - if value.is_a? Hash - time = Time.zone.local(*[:year, :month, :day, :hour, :minute, :second].collect { |part| value[part].to_i }) rescue nil - time.send(conversion) if time - elsif value.respond_to?(:strftime) - if conversion == :to_time - # Explicitly get the current zone, because TimeWithZone#to_time in rails 3.2.3 returns UTC. - # https://github.com/rails/rails/pull/2453 - value.to_time.in_time_zone - else - value.send(conversion) + def translate_days_and_months(value, format) + translated = '' + tables_for_translating_days_and_months(format).each do |table| + regexp = Regexp.union(table.keys) + index = value.index(regexp) + next unless index + translated << value.slice!(0...index) + value.sub!(regexp) do |str| + translated << table[str] + '' end - elsif conversion == :to_date - Date.strptime(value, I18n.t("date.formats.#{column.options[:format] || :default}")) rescue nil + end + translated << value + end + + def format_for_datetime(column, value) + parts = Date._parse(value) + if ActiveScaffold.js_framework == :jquery + format = I18n.translate "time.formats.#{column.options[:format] || :picker}", :default => '' + end + + if format.blank? + time_parts = [[:hour, '%H'], [:min, '%M'], [:sec, '%S']].map do |part, format_part| + format_part if parts[part].present? + end.compact + format = "#{I18n.t('date.formats.default')} #{time_parts.join(':')} #{'%z' if parts[:offset].present?}" else - parts = Date._parse(value) - format = I18n.translate "time.formats.#{column.options[:format] || :picker}", :default => '' if ActiveScaffold.js_framework == :jquery - if format.blank? - time_parts = [[:hour, '%H'], [:min, '%M'], [:sec, '%S']].collect { |part, format_part| format_part if parts[part].present? }.compact - format = "#{I18n.t('date.formats.default')} #{time_parts.join(':')} #{'%z' if parts[:offset].present?}" - else - if parts[:hour] - [[:min, '%M'], [:sec, '%S']].each { |part, f| format.gsub!(":#{f}", '') unless parts[part].present? } + [[:hour, '%H'], [:min, ':%M'], [:sec, ':%S']].each do |part, f| + format.gsub!(f, '') if parts[part].blank? + end + format += ' %z' if parts[:offset].present? && format !~ /%z/i + end + + format.gsub!(/.*(?=%H)/, '') if !parts[:year] && !parts[:month] && !parts[:mday] + [format, parts[:offset]] + end + + def condition_value_for_datetime(column, value, conversion = :to_time) + unless value.nil? || value.blank? + if value.is_a? Hash + time = Time.zone.local(*%i[year month day hour minute second].collect { |part| value[part].to_i }) rescue nil + time.send(conversion) if time + elsif value.respond_to?(:strftime) + if conversion == :to_time + # Explicitly get the current zone, because TimeWithZone#to_time in rails 3.2.3 returns UTC. + # https://github.com/rails/rails/pull/2453 + value.to_time.in_time_zone else - value += ' 00:00:00' + value.send(conversion) end - format += ' %z' if parts[:offset].present? && format !~ /%z/i + elsif conversion == :to_date + format = I18n.t("date.formats.#{column.options[:format] || :default}") + format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m + value = translate_days_and_months(value, format) if I18n.locale != :en + Date.strptime(value, format) rescue nil + elsif value.include?('T') + Time.zone.parse(value) + else # datetime + format, offset = format_for_datetime(column, value) + format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m + value = translate_days_and_months(value, format) if I18n.locale != :en + time = DateTime.strptime(value, format) rescue nil + if time + time = Time.zone.local_to_utc(time).in_time_zone unless offset + time = time.send(conversion) unless conversion == :to_time + end + time end - if !parts[:year] && !parts[:month] && !parts[:mday] - value = "#{Date.today.strftime(format.gsub(/%[HI].*/, ''))} #{value}" - end - value = translate_days_and_months(value, format) if I18n.locale != :en - time = DateTime.strptime(value, format) rescue nil - if time - time = Time.zone.local_to_utc(time).in_time_zone unless parts[:offset] - time = time.send(conversion) unless conversion == :to_time - end - time - end unless value.nil? || value.blank? + end end def condition_value_for_numeric(column, value) return value if value.nil? value = column.number_to_native(value) if column.options[:format] && column.search_ui != :number case (column.search_ui || column.column.type) when :integer then value.to_i rescue value ? 1 : 0 when :float then value.to_f when :decimal if Rails.version >= '4.2.0' - ActiveRecord::Type::Decimal.new.type_cast_from_user(value) + ::ActiveRecord::Type::Decimal.new.type_cast_from_user(value) else - ActiveRecord::ConnectionAdapters::Column.value_to_decimal(value) + ::ActiveRecord::ConnectionAdapters::Column.value_to_decimal(value) end else value end end @@ -232,36 +315,27 @@ ['%{search_sql} is null', []] when 'not_null' ['%{search_sql} is not null', []] end end - - def like_pattern(text_search) - case text_search - when :full then '%?%' - when :start then '?%' - when :end then '%?' - else '?' - end - end end NUMERIC_COMPARATORS = [ '=', '>=', '<=', '>', '<', '!=', 'BETWEEN' - ] + ].freeze STRING_COMPARATORS = { :contains => '%?%', :begins_with => '?%', :ends_with => '%?' - } - NULL_COMPARATORS = %w(null not_null) + }.freeze + NULL_COMPARATORS = %w[null not_null].freeze def self.included(klass) klass.extend ClassMethods end @@ -275,20 +349,10 @@ attr_writer :active_scaffold_preload def active_scaffold_preload @active_scaffold_preload ||= [] end - def active_scaffold_includes=(value) - ActiveSupport::Deprecation.warn "active_scaffold_includes doesn't exist anymore, use active_scaffold_preload, active_scaffold_outer_joins or active_scaffold_references" - self.active_scaffold_preload = value - end - - def active_scaffold_includes - ActiveSupport::Deprecation.warn "active_scaffold_includes doesn't exist anymore, use active_scaffold_preload, active_scaffold_outer_joins or active_scaffold_references" - active_scaffold_preload - end - attr_writer :active_scaffold_habtm_joins def active_scaffold_habtm_joins @active_scaffold_habtm_joins ||= [] end @@ -301,35 +365,37 @@ def active_scaffold_references @active_scaffold_references ||= [] end # Override this method on your controller to define conditions to be used when querying a recordset (e.g. for List). The return of this method should be any format compatible with the :conditions clause of ActiveRecord::Base's find. - def conditions_for_collection - end + def conditions_for_collection; end # Override this method on your controller to define joins to be used when querying a recordset (e.g. for List). The return of this method should be any format compatible with the :joins clause of ActiveRecord::Base's find. - def joins_for_collection - end + def joins_for_collection; end # Override this method on your controller to provide custom finder options to the find() call. The return of this method should be a hash. def custom_finder_options {} end + def active_scaffold_embedded_conditions + params_hash active_scaffold_embedded_params[:conditions] + end + def all_conditions [ id_condition, # for list with id (e.g. /users/:id/index) active_scaffold_conditions, # from the search modules conditions_for_collection, # from the dev conditions_from_params, # from the parameters (e.g. /users/list?first_name=Fred) conditions_from_constraints, # from any constraints (embedded scaffolds) - active_scaffold_session_storage['conditions'] # embedding conditions (weaker constraints) + active_scaffold_embedded_conditions # embedding conditions (weaker constraints) ].reject(&:blank?) end def id_condition - {active_scaffold_config.model.primary_key => params[:id]} if params[:id] + {active_scaffold_config.primary_key => params[:id]} if params[:id] end # returns a single record (the given id) but only if it's allowed for the specified security options. # security options can be a hash for authorized_for? method or a value to check as a :crud_type # accomplishes this by checking model.#{action}_authorized? @@ -337,39 +403,43 @@ record = klass.find(id) security_options = {:crud_type => security_options.to_sym} unless security_options.is_a? Hash raise ActiveScaffold::RecordNotAllowed, "#{klass} with id = #{id}" unless record.authorized_for? security_options record end + # valid options may include: # * :sorting - a Sorting DataStructure (basically an array of hashes of field => direction, e.g. [{:field1 => 'asc'}, {:field2 => 'desc'}]). please note that multi-column sorting has some limitations: if any column in a multi-field sort uses method-based sorting, it will be ignored. method sorting only works for single-column sorting. # * :per_page # * :page def finder_options(options = {}) search_conditions = all_conditions - full_includes = (active_scaffold_references.blank? ? nil : active_scaffold_references) # create a general-use options array that's compatible with Rails finders finder_options = { - :reorder => options[:sorting].try(:clause), - :conditions => search_conditions, - :joins => joins_for_finder, - :outer_joins => active_scaffold_outer_joins, - :preload => active_scaffold_preload, - :includes => full_includes, - :select => options[:select] + :reorder => options[:sorting].try(:clause, (grouped_columns_calculations if grouped_search?)), + :conditions => search_conditions } - if Rails::VERSION::MAJOR >= 4 - finder_options.merge!(:references => active_scaffold_references) + if active_scaffold_config.mongoid? + finder_options[:includes] = [active_scaffold_references, active_scaffold_preload].compact.flatten.uniq.presence + else + finder_options.merge!( + :joins => joins_for_finder, + :left_joins => active_scaffold_outer_joins, + :preload => active_scaffold_preload, + :includes => active_scaffold_references.presence, + :references => active_scaffold_references.presence, + :select => options[:select] + ) end finder_options.merge! custom_finder_options finder_options end def count_items(query, find_options = {}, count_includes = nil) - count_includes ||= find_options[:includes] unless find_options[:conditions].blank? - options = find_options.reject { |k, _| [:select, :reorder].include? k } + count_includes ||= find_options[:includes] if find_options[:conditions].present? + options = find_options.reject { |k, _| %i[select reorder order].include? k } # NOTE: we must use includes in the count query, because some conditions may reference other tables options[:includes] = count_includes count = append_to_query(query, options).count @@ -394,24 +464,24 @@ count = count_items(query, find_options, options[:count_includes]) end query = append_to_query(query, find_options) # we build the paginator differently for method- and sql-based sorting - if options[:sorting] && options[:sorting].sorts_by_method? - pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page| - calculate_last_modified(query) - sorted_collection = sort_collection_by_column(query.to_a, *options[:sorting].first) - sorted_collection = sorted_collection.slice(offset, per_page) if options[:pagination] - sorted_collection - end - else - pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page| - query = append_to_query(query, :offset => offset, :limit => per_page) if options[:pagination] - calculate_last_modified(query) - query - end - end + pager = if options[:sorting] && options[:sorting].sorts_by_method? + ::Paginator.new(count, options[:per_page]) do |offset, per_page| + calculate_last_modified(query) + sorted_collection = sort_collection_by_column(query.to_a, *options[:sorting].first) + sorted_collection = sorted_collection.slice(offset, per_page) if options[:pagination] + sorted_collection + end + else + ::Paginator.new(count, options[:per_page]) do |offset, per_page| + query = append_to_query(query, :offset => offset, :limit => per_page) if options[:pagination] + calculate_last_modified(query) + query + end + end pager.page(options[:page]) end def calculate_last_modified(query) return unless conditional_get_support? && query.klass.columns_hash['updated_at'] @@ -419,42 +489,38 @@ end def calculate_query conditions = all_conditions includes = active_scaffold_config.list.count_includes - includes ||= active_scaffold_references unless conditions.blank? - outer_joins = active_scaffold_outer_joins - outer_joins += includes if includes - primary_key = active_scaffold_config.model.primary_key - subquery = append_to_query(beginning_of_chain, :conditions => conditions, :joins => joins_for_finder, :outer_joins => outer_joins, :select => active_scaffold_config.columns[primary_key].field) - subquery = subquery.unscope(:order) if Rails::VERSION::MAJOR >= 4 + includes ||= active_scaffold_references if conditions.present? + left_joins = active_scaffold_outer_joins + left_joins += includes if includes + primary_key = active_scaffold_config.primary_key + subquery = append_to_query(beginning_of_chain, :conditions => conditions, :joins => joins_for_finder, :left_joins => left_joins, :select => active_scaffold_config.columns[primary_key].field) + subquery = subquery.unscope(:order) active_scaffold_config.model.where(primary_key => subquery) end def append_to_query(relation, options) - options.assert_valid_keys :where, :select, :having, :group, :reorder, :limit, :offset, :joins, :outer_joins, :includes, :lock, :readonly, :from, :conditions, :preload, (:references if Rails::VERSION::MAJOR >= 4) + options.assert_valid_keys :where, :select, :having, :group, :reorder, :order, :limit, :offset, :joins, :left_joins, :left_outer_joins, :includes, :lock, :readonly, :from, :conditions, :preload, :references relation = options.reject { |_, v| v.blank? }.inject(relation) do |rel, (k, v)| k == :conditions ? apply_conditions(rel, *v) : rel.send(k, v) end - if options[:outer_joins].present? - if Rails::VERSION::MAJOR >= 4 - relation.distinct_value = true - else - relation = relation.uniq - end + if options[:left_outer_joins].present? || options[:left_joins].present? + relation.distinct_value = true end relation end def joins_for_finder case joins_for_collection - when String - [joins_for_collection] - when Array - joins_for_collection - else - [] + when String + [joins_for_collection] + when Array + joins_for_collection + else + [] end + active_scaffold_habtm_joins end def apply_conditions(relation, *conditions) conditions.reject(&:blank?).inject(relation) do |rel, condition| @@ -472,10 +538,10 @@ collection = collection.sort_by do |record| value = (sorter.is_a? Proc) ? record.instance_eval(&sorter) : record.instance_eval(sorter.to_s) value = '' if value.nil? value end - collection.reverse! if order.downcase == 'desc' + collection.reverse! if order.casecmp('DESC').zero? collection end end end