Module: ActiveScaffold::Finder::ClassMethods

Defined in:
lib/active_scaffold/finder.rb

Defined Under Namespace

Modules: ActiveRecord, Mongoid

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extended(klass) ⇒ Object

[View source]

8
9
10
11
12
13
14
15
# File 'lib/active_scaffold/finder.rb', line 8

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

Instance Method Details

#condition_for_column(column, value, text_search = :full) ⇒ Object

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

[View source]

94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/active_scaffold/finder.rb', line 94

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 respond_to?("condition_for_#{column.name}_column")
    return send("condition_for_#{column.name}_column", column, value, like_pattern)
  end
  return unless column&.search_sql && value.present?
  search_ui = column.search_ui || column.column_type
  begin
    sql, *values =
      if search_ui && respond_to?("condition_for_#{search_ui}_type")
        send("condition_for_#{search_ui}_type", column, value, like_pattern)
      elsif column.search_sql.instance_of? Proc
        column.search_sql.call(value)
      else
        condition_for_search_ui(column, value, like_pattern, search_ui)
      end
    return nil unless sql

    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
    Rails.logger.error "#{e.class.name}: #{e.message} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{name}"
    raise e
  end
end

#condition_for_datetime(column, value, like_pattern = nil) ⇒ Object

[View source]

314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/active_scaffold/finder.rb', line 314

def condition_for_datetime(column, value, like_pattern = nil)
  conversion = datetime_conversion_for_condition(column)
  from_value = condition_value_for_datetime(column, value[:from], conversion)
  to_value = condition_value_for_datetime(column, value[:to], conversion)

  if from_value.nil? && to_value.nil?
    nil
  elsif !from_value
    ['%<search_sql>s <= ?', to_value]
  elsif !to_value
    ['%<search_sql>s >= ?', from_value]
  else
    ['%<search_sql>s BETWEEN ? AND ?', from_value, to_value]
  end
end

#condition_for_null_type(column, value, like_pattern = nil) ⇒ Object

[View source]

338
339
340
341
342
343
344
345
# File 'lib/active_scaffold/finder.rb', line 338

def condition_for_null_type(column, value, like_pattern = nil)
  case value.to_s
  when 'null'
    ['%<search_sql>s is null', []]
  when 'not_null'
    ['%<search_sql>s is not null', []]
  end
end

#condition_for_numeric(column, value) ⇒ Object

[View source]

144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/active_scaffold/finder.rb', line 144

def condition_for_numeric(column, value)
  if !value.is_a?(Hash)
    ['%<search_sql>s = ?', condition_value_for_numeric(column, value)]
  elsif ActiveScaffold::Finder::NULL_COMPARATORS.include?(value[:opt])
    condition_for_null_type(column, value[:opt])
  elsif value[:from].blank? || !ActiveScaffold::Finder::NUMERIC_COMPARATORS.include?(value[:opt])
    nil
  elsif value[:opt] == 'BETWEEN'
    ['(%<search_sql>s BETWEEN ? AND ?)', condition_value_for_numeric(column, value[:from]), condition_value_for_numeric(column, value[:to])]
  else
    ["%<search_sql>s #{value[:opt]} ?", condition_value_for_numeric(column, value[:from])]
  end
end

#condition_for_range(column, value, like_pattern = nil) ⇒ Object

[View source]

158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/active_scaffold/finder.rb', line 158

def condition_for_range(column, value, like_pattern = nil)
  if !value.is_a?(Hash)
    if column.text?
      ["%<search_sql>s #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)]
    else
      ['%<search_sql>s = ?', ActiveScaffold::Core.column_type_cast(value, column.column)]
    end
  elsif ActiveScaffold::Finder::NULL_COMPARATORS.include?(value[:opt])
    condition_for_null_type(column, value[:opt], like_pattern)
  elsif value[:from].blank?
    nil
  elsif ActiveScaffold::Finder::STRING_COMPARATORS.values.include?(value[:opt])
    ["%<search_sql>s #{ActiveScaffold::Finder.like_operator} ?", value[:opt].sub('?', value[:from])]
  elsif value[:opt] == 'BETWEEN'
    ['(%<search_sql>s BETWEEN ? AND ?)', value[:from], value[:to]]
  elsif ActiveScaffold::Finder::NUMERIC_COMPARATORS.include?(value[:opt])
    ["%<search_sql>s #{value[:opt]} ?", value[:from]]
  end
end

#condition_for_record_select_type(column, value, like_pattern = nil) ⇒ Object

[View source]

330
331
332
333
334
335
336
# File 'lib/active_scaffold/finder.rb', line 330

def condition_for_record_select_type(column, value, like_pattern = nil)
  if value.is_a?(Array)
    ['%<search_sql>s IN (?)', value]
  else
    ['%<search_sql>s = ?', value]
  end
end

#condition_for_search_ui(column, value, like_pattern, search_ui) ⇒ Object

[View source]

122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/active_scaffold/finder.rb', line 122

def condition_for_search_ui(column, value, like_pattern, search_ui)
  case search_ui
  when :boolean, :checkbox
    ['%<search_sql>s = ?', column.column ? ActiveScaffold::Core.column_type_cast(value, column.column) : value]
  when :integer, :decimal, :float
    condition_for_numeric(column, value)
  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
    values = Array(value).select(&:present?)
    ['%<search_sql>s in (?)', values] if values.present?
  else
    if column.text?
      ["%<search_sql>s #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)]
    else
      ['%<search_sql>s = ?', ActiveScaffold::Core.column_type_cast(value, column.column)]
    end
  end
end

#condition_value_for_datetime(column, value, conversion = :to_time) ⇒ Object

[View source]

266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/active_scaffold/finder.rb', line 266

def condition_value_for_datetime(column, value, conversion = :to_time)
  return if value.nil? || value.blank?
  if value.is_a? Hash
    local_time_from_hash(value, conversion)
  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)
    end
  elsif conversion == :to_date
    parse_date_with_format(value, column.options[:format])
  elsif value.include?('T')
    Time.zone.parse(value)
  else # datetime
    time = parse_time_with_format(value, *format_for_datetime(column, value))
    conversion == :to_time ? time : time.send(conversion)
  end
end

#condition_value_for_numeric(column, value) ⇒ Object

[View source]

288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/active_scaffold/finder.rb', line 288

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
    if value.is_a?(TrueClass) || value.is_a?(FalseClass)
      value ? 1 : 0
    else
      value.to_i
    end
  when :float     then value.to_f
  when :decimal
    ::ActiveRecord::Type::Decimal.new.type_cast_from_user(value)
  else
    value
  end
end

#conditions_for_columns(tokens, columns, text_search = :full) ⇒ Object

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.

[View source]

20
21
22
23
24
25
26
27
# File 'lib/active_scaffold/finder.rb', line 20

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?

  tokens = [tokens] if tokens.is_a? String
  tokens = type_casted_tokens(tokens, columns, like_pattern(text_search))
  create_conditions_for_columns(tokens, columns)
end

#datetime_conversion_for_condition(column) ⇒ Object

[View source]

306
307
308
309
310
311
312
# File 'lib/active_scaffold/finder.rb', line 306

def datetime_conversion_for_condition(column)
  if column.column
    column.column.type == :date ? :to_date : :to_time
  else
    :to_time
  end
end

#format_for_datetime(column, value) ⇒ Object

[View source]

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/active_scaffold/finder.rb', line 209

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
    [[: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

#local_time_from_hash(value, conversion = :to_time) ⇒ Object

[View source]

231
232
233
234
235
236
237
238
# File 'lib/active_scaffold/finder.rb', line 231

def local_time_from_hash(value, conversion = :to_time)
  time = Time.zone.local(*%i[year month day hour minute second].collect { |part| value[part].to_i })
  time.send(conversion)
rescue StandardError => e
  message = "Error creating time from #{value.inspect}:"
  Rails.logger.warn "#{message}\n#{e.message}\n#{e.backtrace.join("\n")}"
  nil
end

#parse_date_with_format(value, format_name) ⇒ Object

[View source]

240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/active_scaffold/finder.rb', line 240

def parse_date_with_format(value, format_name)
  format = I18n.t("date.formats.#{format_name || :default}")
  format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m
  en_value = I18n.locale == :en ? value : translate_days_and_months(value, format)
  Date.strptime(en_value, format)
rescue StandardError => e
  message = "Error parsing date from #{en_value}"
  message << " (#{value})" if en_value != value
  message << ", with format #{format}" if format
  Rails.logger.warn "#{message}:\n#{e.message}\n#{e.backtrace.join("\n")}"
  nil
end

#parse_time_with_format(value, format, offset) ⇒ Object

[View source]

253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/active_scaffold/finder.rb', line 253

def parse_time_with_format(value, format, offset)
  format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m
  en_value = I18n.locale == :en ? value : translate_days_and_months(value, format)
  time = Time.strptime(en_value, format)
  offset ? time : Time.zone.local_to_utc(time).in_time_zone
rescue StandardError => e
  message = "Error parsing time from #{en_value}"
  message << " (#{value})" if en_value != value
  message << ", with format #{format}" if format
  Rails.logger.warn "#{message}:\n#{e.message}\n#{e.backtrace.join("\n")}"
  nil
end

#tables_for_translating_days_and_months(format) ⇒ Object

[View source]

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/active_scaffold/finder.rb', line 178

def tables_for_translating_days_and_months(format)
  # rubocop:disable Style/FormatStringToken
  keys = {
    '%A' => 'date.day_names',
    '%a' => 'date.abbr_day_names',
    '%B' => 'date.month_names',
    '%b' => 'date.abbr_month_names'
  }
  # rubocop:enable Style/FormatStringToken
  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
end

#translate_days_and_months(value, format) ⇒ Object

[View source]

194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/active_scaffold/finder.rb', line 194

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
  end
  translated << value
end

#type_casted_tokens(tokens, columns, like_pattern) ⇒ Object

[View source]

29
30
31
32
33
34
35
# File 'lib/active_scaffold/finder.rb', line 29

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
end