active_scaffold 3.7.0 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +20 -0
  3. data/README.md +2 -0
  4. data/app/assets/javascripts/jquery/active_scaffold.js +68 -62
  5. data/app/assets/stylesheets/active_scaffold_layout.css +1 -1
  6. data/app/views/active_scaffold_overrides/_form_association_record.html.erb +2 -1
  7. data/app/views/active_scaffold_overrides/_render_field.js.erb +9 -6
  8. data/config/locales/de.yml +6 -3
  9. data/config/locales/en.yml +3 -0
  10. data/config/locales/es.yml +3 -0
  11. data/config/locales/fr.yml +9 -6
  12. data/config/locales/hu.yml +20 -17
  13. data/config/locales/ja.yml +25 -22
  14. data/config/locales/ru.yml +17 -14
  15. data/lib/active_scaffold/actions/update.rb +3 -3
  16. data/lib/active_scaffold/attribute_params.rb +7 -17
  17. data/lib/active_scaffold/bridges/active_storage/form_ui.rb +6 -6
  18. data/lib/active_scaffold/bridges/active_storage/list_ui.rb +7 -7
  19. data/lib/active_scaffold/bridges/ancestry/ancestry_bridge.rb +2 -2
  20. data/lib/active_scaffold/bridges/calendar_date_select/as_cds_bridge.rb +12 -14
  21. data/lib/active_scaffold/bridges/carrierwave/form_ui.rb +2 -2
  22. data/lib/active_scaffold/bridges/carrierwave/list_ui.rb +1 -1
  23. data/lib/active_scaffold/bridges/chosen/helpers.rb +10 -10
  24. data/lib/active_scaffold/bridges/country_select/country_select_bridge_helper.rb +7 -7
  25. data/lib/active_scaffold/bridges/date_picker/ext.rb +20 -9
  26. data/lib/active_scaffold/bridges/date_picker/helper.rb +5 -5
  27. data/lib/active_scaffold/bridges/date_picker.rb +2 -0
  28. data/lib/active_scaffold/bridges/dragonfly/form_ui.rb +3 -3
  29. data/lib/active_scaffold/bridges/dragonfly/list_ui.rb +5 -5
  30. data/lib/active_scaffold/bridges/file_column/form_ui.rb +1 -1
  31. data/lib/active_scaffold/bridges/file_column/list_ui.rb +3 -3
  32. data/lib/active_scaffold/bridges/file_column/test/functional/file_column_keep_test.rb +1 -1
  33. data/lib/active_scaffold/bridges/paperclip/form_ui.rb +3 -3
  34. data/lib/active_scaffold/bridges/paperclip/list_ui.rb +1 -1
  35. data/lib/active_scaffold/bridges/record_select/helpers.rb +15 -15
  36. data/lib/active_scaffold/bridges/tiny_mce/helpers.rb +6 -6
  37. data/lib/active_scaffold/bridges/usa_state_select/usa_state_select_helper.rb +5 -5
  38. data/lib/active_scaffold/bridges.rb +0 -3
  39. data/lib/active_scaffold/constraints.rb +22 -7
  40. data/lib/active_scaffold/core.rb +5 -3
  41. data/lib/active_scaffold/data_structures/column.rb +108 -23
  42. data/lib/active_scaffold/engine.rb +15 -0
  43. data/lib/active_scaffold/extensions/routing_mapper.rb +1 -0
  44. data/lib/active_scaffold/finder.rb +142 -27
  45. data/lib/active_scaffold/helpers/controller_helpers.rb +9 -4
  46. data/lib/active_scaffold/helpers/form_column_helpers.rb +114 -94
  47. data/lib/active_scaffold/helpers/human_condition_helpers.rb +48 -14
  48. data/lib/active_scaffold/helpers/list_column_helpers.rb +34 -18
  49. data/lib/active_scaffold/helpers/search_column_helpers.rb +131 -55
  50. data/lib/active_scaffold/helpers/show_column_helpers.rb +6 -6
  51. data/lib/active_scaffold/orm_checks.rb +21 -1
  52. data/lib/active_scaffold/version.rb +1 -1
  53. data/lib/active_scaffold.rb +3 -2
  54. data/test/bridges/date_picker_test.rb +3 -2
  55. data/test/bridges/paperclip_test.rb +3 -2
  56. data/test/bridges/tiny_mce_test.rb +4 -2
  57. data/test/helpers/form_column_helpers_test.rb +7 -5
  58. data/test/helpers/search_column_helpers_test.rb +2 -1
  59. data/test/misc/constraints_test.rb +1 -0
  60. data/test/misc/finder_test.rb +38 -0
  61. metadata +2 -6
  62. data/config/brakeman.ignore +0 -26
  63. data/config/brakeman.yml +0 -3
  64. data/config/i18n-tasks.yml +0 -121
  65. data/lib/active_scaffold/bridges/shared/date_bridge.rb +0 -221
@@ -1,9 +1,6 @@
1
1
  module ActiveScaffold
2
2
  module Bridges
3
3
  ActiveScaffold.autoload_subdir('bridges', self)
4
- module Shared
5
- autoload :DateBridge, 'active_scaffold/bridges/shared/date_bridge'
6
- end
7
4
 
8
5
  mattr_accessor :bridges
9
6
  mattr_accessor :bridges_run
@@ -11,13 +11,27 @@ module ActiveScaffold
11
11
  @active_scaffold_constraints ||= active_scaffold_embedded_params[:constraints] || {}
12
12
  end
13
13
 
14
+ def register_constraint?(column_name, value)
15
+ if params_hash?(value)
16
+ false
17
+ elsif value.is_a?(Array)
18
+ column = active_scaffold_config.columns[column_name]
19
+ column && value.size > (column.association&.polymorphic? ? 2 : 1)
20
+ else
21
+ true
22
+ end
23
+ end
24
+
14
25
  # For each enabled action, adds the constrained columns to the ActionColumns object (if it exists).
15
26
  # This lets the ActionColumns object skip constrained columns.
16
27
  #
17
- # If the constraint value is a Hash, then we assume the constraint is a multi-level association constraint (the reverse of a has_many :through) and we do NOT register the constraint column.
28
+ # If the constraint value is a Hash, then we assume the constraint is a multi-level association constraint
29
+ # (the reverse of a has_many :through) and we do NOT register the constraint column.
30
+ # If the constraint value is an Array, or Array with more than 2 items for polymorphic column,
31
+ # we do NOT register the constraint column, as records will have different values in the column.
18
32
  def register_constraints_with_action_columns(constrained_fields = nil)
19
33
  constrained_fields ||= []
20
- constrained_fields |= active_scaffold_constraints.reject { |_, v| params_hash?(v) }.keys.collect(&:to_sym)
34
+ constrained_fields |= active_scaffold_constraints.select { |k, v| register_constraint?(k, v) }.keys.collect(&:to_sym)
21
35
  exclude_actions = []
22
36
  %i[list update].each do |action_name|
23
37
  if active_scaffold_config.actions.include? action_name
@@ -111,10 +125,10 @@ module ActiveScaffold
111
125
  value = association.klass.find(value).send(association.primary_key) if association.primary_key
112
126
 
113
127
  if association.polymorphic?
114
- unless value.is_a?(Array) && value.size == 2
128
+ unless value.is_a?(Array) && value.size >= 2
115
129
  raise ActiveScaffold::MalformedConstraint, polymorphic_constraint_error(association), caller
116
130
  end
117
- condition = {table => {association.foreign_type => value[0], field => value[1]}}
131
+ condition = {table => {association.foreign_type => value[0], field => value.size == 2 ? value[1] : value[1..-1]}}
118
132
  else
119
133
  condition = {table => {field.to_s => value}}
120
134
  end
@@ -149,11 +163,12 @@ module ActiveScaffold
149
163
  if column.association.collection?
150
164
  record.send(k.to_s).send(:<<, column.association.klass.find(v))
151
165
  elsif column.association.polymorphic?
152
- unless v.is_a?(Array) && v.size == 2
166
+ unless v.is_a?(Array) && v.size >= 2
153
167
  raise ActiveScaffold::MalformedConstraint, polymorphic_constraint_error(column.association), caller
154
168
  end
155
- record.send("#{k}=", v[0].constantize.find(v[1]))
156
- elsif !column.association.source_reflection&.options&.include?(:through) # regular singular association, or one-level through association
169
+ record.send("#{k}=", v[0].constantize.find(v[1])) if v.size == 2
170
+ elsif !column.association.source_reflection&.options&.include?(:through) && # regular singular association, or one-level through association
171
+ !v.is_a?(Array)
157
172
  record.send("#{k}=", column.association.klass.find(v))
158
173
 
159
174
  # setting the belongs_to side of a has_one isn't safe. if the has_one was already
@@ -257,6 +257,8 @@ module ActiveScaffold
257
257
  def self.column_type_cast(value, column)
258
258
  if defined?(ActiveRecord) && column.is_a?(ActiveRecord::ConnectionAdapters::Column)
259
259
  active_record_column_type_cast(value, column)
260
+ elsif defined?(ActiveModel) && column.is_a?(ActiveModel::Attribute)
261
+ active_record_column_type_cast(value, column.type)
260
262
  elsif defined?(Mongoid) && column.is_a?(Mongoid::Fields::Standard)
261
263
  mongoid_column_type_cast(value, column)
262
264
  else
@@ -269,9 +271,9 @@ module ActiveScaffold
269
271
  column.type.evolve value
270
272
  end
271
273
 
272
- def self.active_record_column_type_cast(value, column)
273
- return Time.zone.at(value.to_i) if value =~ /\A\d+\z/ && %i[time datetime].include?(column.type)
274
- cast_type = ActiveRecord::Type.lookup column.type
274
+ def self.active_record_column_type_cast(value, column_or_type)
275
+ return Time.zone.at(value.to_i) if value =~ /\A\d+\z/ && %i[time datetime].include?(column_or_type.type)
276
+ cast_type = column_or_type.is_a?(ActiveRecord::ConnectionAdapters::Column) ? ActiveRecord::Type.lookup(column_or_type.type) : column_or_type
275
277
  cast_type ? cast_type.cast(value) : value
276
278
  end
277
279
  end
@@ -144,24 +144,58 @@ module ActiveScaffold::DataStructures
144
144
  # supported options:
145
145
  # * for association columns
146
146
  # * :select - displays a simple <select> or a collection of checkboxes to (dis)associate records
147
- attr_accessor :form_ui
147
+ attr_reader :form_ui
148
148
 
149
- attr_writer :list_ui
149
+ attr_reader :form_ui_options
150
+
151
+ # value must be a Symbol, or an Array of form_ui and options hash which will be used with form_ui only
152
+ def form_ui=(value)
153
+ check_valid_action_ui_params(value)
154
+ @form_ui, @form_ui_options = *value
155
+ end
156
+
157
+ # value must be a Symbol, or an Array of list_ui and options hash which will be used with list_ui only
158
+ def list_ui=(value)
159
+ check_valid_action_ui_params(value)
160
+ @list_ui, @list_ui_options = *value
161
+ end
150
162
 
151
163
  def list_ui
152
164
  @list_ui || form_ui
153
165
  end
154
166
 
155
- attr_writer :show_ui
167
+ def list_ui_options
168
+ @list_ui ? @list_ui_options : form_ui_options
169
+ end
170
+
171
+ # value must be a Symbol, or an Array of show_ui and options hash which will be used with show_ui only
172
+ def show_ui=(value)
173
+ check_valid_action_ui_params(value)
174
+ @show_ui, @show_ui_options = *value
175
+ end
176
+
156
177
  def show_ui
157
178
  @show_ui || list_ui
158
179
  end
159
180
 
160
- attr_writer :search_ui
181
+ def show_ui_options
182
+ @show_ui ? @show_ui_options : list_ui_options
183
+ end
184
+
185
+ # value must be a Symbol, or an Array of search_ui and options hash which will be used with search_ui only
186
+ def search_ui=(value)
187
+ check_valid_action_ui_params(value)
188
+ @search_ui, @search_ui_options = *value
189
+ end
190
+
161
191
  def search_ui
162
192
  @search_ui || @form_ui || (:select if association && !association.polymorphic?)
163
193
  end
164
194
 
195
+ def search_ui_options
196
+ @search_ui ? @search_ui_options : form_ui_options
197
+ end
198
+
165
199
  # a place to store dev's column specific options
166
200
  attr_writer :options
167
201
  def options
@@ -340,6 +374,9 @@ module ActiveScaffold::DataStructures
340
374
  @name = name.to_sym
341
375
  @active_record_class = active_record_class
342
376
  @column = _columns_hash[name.to_s]
377
+ if @column.nil? && active_record? && active_record_class._default_attributes.key?(name.to_s)
378
+ @column = active_record_class._default_attributes[name.to_s]
379
+ end
343
380
  @delegated_association = delegated_association
344
381
  @cache_key = [@active_record_class.name, name].compact.map(&:to_s).join('#')
345
382
  setup_association_info
@@ -356,22 +393,7 @@ module ActiveScaffold::DataStructures
356
393
 
357
394
  @text = @column.nil? || [:string, :text, :citext, String].include?(column_type)
358
395
  @number = false
359
- if @column
360
- if active_record_class.respond_to?(:defined_enums) && active_record_class.defined_enums[name.to_s]
361
- @form_ui = :select
362
- @options = {:options => active_record_class.send(name.to_s.pluralize).keys.map(&:to_sym)}
363
- elsif column_number?
364
- @number = true
365
- @form_ui = :number
366
- @options = {:format => :i18n_number}
367
- else
368
- @form_ui =
369
- case @column.type
370
- when :boolean then :checkbox
371
- when :text then :textarea
372
- end
373
- end
374
- end
396
+ setup_defaults_for_column if @column
375
397
  @allow_add_existing = true
376
398
  @form_ui = self.class.association_form_ui if @association && self.class.association_form_ui
377
399
 
@@ -396,7 +418,7 @@ module ActiveScaffold::DataStructures
396
418
  # just the field (not table.field)
397
419
  def field_name
398
420
  return nil if virtual?
399
- @field_name ||= column ? quoted_field_name(column.name) : association.foreign_key
421
+ @field_name ||= column ? quoted_field_name(column.name) : quoted_field_name(association.foreign_key)
400
422
  end
401
423
 
402
424
  def <=>(other)
@@ -430,7 +452,22 @@ module ActiveScaffold::DataStructures
430
452
  end
431
453
 
432
454
  def default_for_empty_value
433
- (column.null ? nil : column.default) if column
455
+ return nil unless column
456
+ if column.is_a?(ActiveModel::Attribute)
457
+ column.value
458
+ elsif active_record?
459
+ null? ? nil : column.default
460
+ elsif mongoid?
461
+ column.default_val
462
+ end
463
+ end
464
+
465
+ def null?
466
+ if active_record? && !column.is_a?(ActiveModel::Attribute)
467
+ column&.null
468
+ else
469
+ true
470
+ end
434
471
  end
435
472
 
436
473
  # the table.field name for this column, if applicable
@@ -438,6 +475,10 @@ module ActiveScaffold::DataStructures
438
475
  @field ||= quoted_field(field_name)
439
476
  end
440
477
 
478
+ def quoted_foreign_type
479
+ quoted_field(quoted_field_name(association.foreign_type))
480
+ end
481
+
441
482
  def type_for_attribute
442
483
  ActiveScaffold::OrmChecks.type_for_attribute active_record_class, name
443
484
  end
@@ -446,8 +487,39 @@ module ActiveScaffold::DataStructures
446
487
  ActiveScaffold::OrmChecks.column_type active_record_class, name
447
488
  end
448
489
 
490
+ def default_value
491
+ ActiveScaffold::OrmChecks.column_type active_record_class, name
492
+ end
493
+
494
+ def attributes=(opts)
495
+ opts.each do |setting, value|
496
+ send "#{setting}=", value
497
+ end
498
+ end
499
+
500
+ def cast(value)
501
+ ActiveScaffold::OrmChecks.cast active_record_class, name, value
502
+ end
503
+
449
504
  protected
450
505
 
506
+ def setup_defaults_for_column
507
+ if active_record_class.respond_to?(:defined_enums) && active_record_class.defined_enums[name.to_s]
508
+ @form_ui = :select
509
+ @options = {:options => active_record_class.send(name.to_s.pluralize).keys.map(&:to_sym)}
510
+ elsif column_number?
511
+ @number = true
512
+ @form_ui = :number
513
+ @options = {:format => :i18n_number}
514
+ else
515
+ @form_ui =
516
+ case column_type
517
+ when :boolean then null? ? :boolean : :checkbox
518
+ when :text then :textarea
519
+ end
520
+ end
521
+ end
522
+
451
523
  def setup_association_info
452
524
  assoc = active_record_class.reflect_on_association(name)
453
525
  @association =
@@ -506,7 +578,7 @@ module ActiveScaffold::DataStructures
506
578
  end
507
579
 
508
580
  def column_number?
509
- return %i[float decimal integer].include? @column.type if active_record?
581
+ return %i[float decimal integer].include? column_type if active_record?
510
582
  return @column.type < Numeric if mongoid?
511
583
  end
512
584
 
@@ -560,5 +632,18 @@ module ActiveScaffold::DataStructures
560
632
  300
561
633
  end
562
634
  end
635
+
636
+ def check_valid_action_ui_params(value)
637
+ return true if valid_action_ui_params?(value)
638
+ raise ArgumentError, 'value must be a Symbol, or an array of Symbol and Hash'
639
+ end
640
+
641
+ def valid_action_ui_params?(value)
642
+ if value.is_a?(Array)
643
+ value.size <= 2 && value[0].is_a?(Symbol) && (value[1].nil? || value[1].is_a?(Hash))
644
+ else
645
+ value.nil? || value.is_a?(Symbol)
646
+ end
647
+ end
563
648
  end
564
649
  end
@@ -2,6 +2,9 @@ module ActiveScaffold
2
2
  class Engine < ::Rails::Engine
3
3
  initializer 'active_scaffold.action_controller' do
4
4
  ActiveSupport.on_load :action_controller do
5
+ require 'active_scaffold/extensions/action_controller_rescueing'
6
+ require 'active_scaffold/extensions/action_controller_rendering'
7
+ require 'active_scaffold/extensions/routing_mapper'
5
8
  include ActiveScaffold::Core
6
9
  include ActiveScaffold::RespondsToParent
7
10
  include ActiveScaffold::Helpers::ControllerHelpers
@@ -12,12 +15,17 @@ module ActiveScaffold
12
15
 
13
16
  initializer 'active_scaffold.action_view' do
14
17
  ActiveSupport.on_load :action_view do
18
+ require 'active_scaffold/extensions/action_view_rendering'
19
+ require 'active_scaffold/extensions/name_option_for_datetime'
15
20
  include ActiveScaffold::Helpers::ViewHelpers
16
21
  end
17
22
  end
18
23
 
19
24
  initializer 'active_scaffold.active_record' do
20
25
  ActiveSupport.on_load :active_record do
26
+ require 'active_scaffold/extensions/to_label'
27
+ require 'active_scaffold/extensions/unsaved_associated'
28
+ require 'active_scaffold/extensions/unsaved_record'
21
29
  include ActiveScaffold::ActiveRecordPermissions::ModelUserAccess::Model
22
30
  module ActiveRecord::Associations
23
31
  Association.send :include, ActiveScaffold::Tableless::Association
@@ -39,5 +47,12 @@ module ActiveScaffold
39
47
  initializer 'active_scaffold.assets' do
40
48
  config.assets.precompile << 'active_scaffold/indicator.gif'
41
49
  end
50
+
51
+ initializer 'active_scaffold.extensions' do
52
+ require 'active_scaffold/extensions/cow_proxy'
53
+ require 'active_scaffold/extensions/ice_nine'
54
+ require 'active_scaffold/extensions/localize'
55
+ require 'active_scaffold/extensions/paginator_extensions'
56
+ end
42
57
  end
43
58
  end
@@ -68,6 +68,7 @@ module ActiveScaffold
68
68
  end
69
69
  end
70
70
  end
71
+ ActiveSupport.run_load_hooks(:active_scaffold_routing, Routing)
71
72
  end
72
73
 
73
74
  module ActionDispatch
@@ -110,15 +110,42 @@ module ActiveScaffold
110
110
  end
111
111
  return nil unless sql
112
112
 
113
- conditions = [column.search_sql.collect { |search_sql| sql % {:search_sql => search_sql} }.join(' OR ')]
114
- conditions += values * column.search_sql.size if values.present?
115
- conditions
113
+ where_values = []
114
+ sql_conditions = []
115
+ column.search_sql.each do |search_sql|
116
+ if search_sql.is_a?(Hash)
117
+ subquery_sql, *subquery_values = subquery_condition(column, sql, search_sql, values)
118
+ sql_conditions << subquery_sql
119
+ where_values.concat subquery_values
120
+ else
121
+ sql_conditions << sql % {:search_sql => search_sql}
122
+ where_values.concat values
123
+ end
124
+ end
125
+ [sql_conditions.join(' OR '), *where_values]
116
126
  rescue StandardError => e
117
127
  Rails.logger.error "#{e.class.name}: #{e.message} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{name}"
118
128
  raise e
119
129
  end
120
130
  end
121
131
 
132
+ def subquery_condition(column, sql, options, values)
133
+ relation, *columns = options[:subquery]
134
+ conditions = [columns.map { |search_sql| sql % {:search_sql => search_sql} }.join(' OR ')]
135
+ conditions += values * columns.size if values.present?
136
+ subquery = relation.where(conditions)
137
+ subquery = subquery.select(relation.primary_key) if subquery.select_values.blank?
138
+
139
+ conditions = [["#{options[:field] || column.field} IN (?)", options[:conditions]&.first].compact.join(' AND ')]
140
+ conditions << subquery
141
+ conditions.concat options[:conditions][1..-1] if options[:conditions]
142
+ if column.association&.polymorphic?
143
+ conditions[0] << " AND #{column.quoted_foreign_type} = ?"
144
+ conditions << relation.name
145
+ end
146
+ conditions
147
+ end
148
+
122
149
  def condition_for_search_ui(column, value, like_pattern, search_ui)
123
150
  case search_ui
124
151
  when :boolean, :checkbox
@@ -133,7 +160,7 @@ module ActiveScaffold
133
160
  condition_for_range(column, value, like_pattern)
134
161
  when :date, :time, :datetime, :timestamp
135
162
  condition_for_datetime(column, value)
136
- when :select, :multi_select, :country, :usa_state, :chosen, :multi_chosen
163
+ when :select, :select_multiple, :draggable, :multi_select, :country, :usa_state, :chosen, :multi_chosen
137
164
  values = Array(value).select(&:present?)
138
165
  ['%<search_sql>s in (?)', values] if values.present?
139
166
  else
@@ -171,7 +198,10 @@ module ActiveScaffold
171
198
  elsif value[:from].blank?
172
199
  nil
173
200
  elsif ActiveScaffold::Finder::STRING_COMPARATORS.values.include?(value[:opt])
174
- ["%<search_sql>s #{ActiveScaffold::Finder.like_operator} ?", value[:opt].sub('?', value[:from])]
201
+ [
202
+ "%<search_sql>s #{'NOT ' if value[:opt].start_with?('not_')}#{ActiveScaffold::Finder.like_operator} ?",
203
+ value[:opt].sub('not_', '').sub('?', value[:from])
204
+ ]
175
205
  elsif value[:opt] == 'BETWEEN'
176
206
  ['(%<search_sql>s BETWEEN ? AND ?)', value[:from], value[:to]]
177
207
  elsif ActiveScaffold::Finder::NUMERIC_COMPARATORS.include?(value[:opt])
@@ -241,14 +271,19 @@ module ActiveScaffold
241
271
  nil
242
272
  end
243
273
 
244
- def parse_date_with_format(value, format_name)
245
- format = I18n.t("date.formats.#{format_name || :default}")
246
- format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m
247
- en_value = I18n.locale == :en ? value : translate_days_and_months(value, format)
248
- Date.strptime(en_value, format)
274
+ def format_for_date(column, value, format_name = column.options[:format])
275
+ if format_name
276
+ format = I18n.t("date.formats.#{format_name}")
277
+ format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m
278
+ en_value = I18n.locale == :en ? value : translate_days_and_months(value, format)
279
+ end
280
+ [en_value || value, format]
281
+ end
282
+
283
+ def parse_date_with_format(value, format)
284
+ Date.strptime(value, *format)
249
285
  rescue StandardError => e
250
- message = "Error parsing date from #{en_value}"
251
- message << " (#{value})" if en_value != value
286
+ message = "Error parsing date from #{value}"
252
287
  message << ", with format #{format}" if format
253
288
  Rails.logger.warn "#{message}:\n#{e.message}\n#{e.backtrace.join("\n")}"
254
289
  nil
@@ -280,7 +315,7 @@ module ActiveScaffold
280
315
  value.send(conversion)
281
316
  end
282
317
  elsif conversion == :to_date
283
- parse_date_with_format(value, column.options[:format])
318
+ parse_date_with_format(*format_for_date(column, value))
284
319
  elsif value.include?('T')
285
320
  Time.zone.parse(value)
286
321
  else # datetime
@@ -292,7 +327,7 @@ module ActiveScaffold
292
327
  def condition_value_for_numeric(column, value)
293
328
  return value if value.nil?
294
329
  value = column.number_to_native(value) if column.options[:format] && column.search_ui != :number
295
- case (column.search_ui || column.column.type)
330
+ case (column.search_ui || column.column_type)
296
331
  when :integer then
297
332
  if value.is_a?(TrueClass) || value.is_a?(FalseClass)
298
333
  value ? 1 : 0
@@ -300,8 +335,7 @@ module ActiveScaffold
300
335
  value.to_i
301
336
  end
302
337
  when :float then value.to_f
303
- when :decimal
304
- ::ActiveRecord::Type::Decimal.new.send(Rails.version < '5.0' ? :type_cast_from_user : :cast, value)
338
+ when :decimal then ::ActiveRecord::Type::Decimal.new.cast(value)
305
339
  else
306
340
  value
307
341
  end
@@ -309,28 +343,102 @@ module ActiveScaffold
309
343
 
310
344
  def datetime_conversion_for_condition(column)
311
345
  if column.column
312
- column.column.type == :date ? :to_date : :to_time
346
+ column.column_type == :date ? :to_date : :to_time
313
347
  else
314
348
  :to_time
315
349
  end
316
350
  end
317
351
 
318
352
  def condition_for_datetime(column, value, like_pattern = nil)
353
+ operator = ActiveScaffold::Finder::NUMERIC_COMPARATORS.include?(value['opt']) && value['opt'] != 'BETWEEN' ? value['opt'] : nil
354
+ from_value, to_value = datetime_from_to(column, value)
355
+
356
+ if column.search_sql.is_a? Proc
357
+ column.search_sql.call(from_value, to_value, operator)
358
+ elsif operator.nil?
359
+ ['%<search_sql>s BETWEEN ? AND ?', from_value, to_value] unless from_value.nil? || to_value.nil?
360
+ else
361
+ ["%<search_sql>s #{value['opt']} ?", from_value] unless from_value.nil?
362
+ end
363
+ end
364
+
365
+ def datetime_from_to(column, value)
319
366
  conversion = datetime_conversion_for_condition(column)
320
- from_value = condition_value_for_datetime(column, value[:from], conversion)
321
- to_value = condition_value_for_datetime(column, value[:to], conversion)
367
+ case value['opt']
368
+ when 'RANGE'
369
+ values = datetime_from_to_for_range(column, value)
370
+ # Avoid calling to_time, not needed and broken on rails >= 4, because return local time instead of UTC
371
+ values.collect!(&conversion) if conversion != :to_time
372
+ values
373
+ when 'PAST', 'FUTURE'
374
+ values = datetime_from_to_for_trend(column, value)
375
+ # Avoid calling to_time, not needed and broken on rails >= 4, because return local time instead of UTC
376
+ values.collect!(&conversion) if conversion != :to_time
377
+ values
378
+ else
379
+ %w[from to].collect { |field| condition_value_for_datetime(column, value[field], conversion) }
380
+ end
381
+ end
322
382
 
323
- if from_value.nil? && to_value.nil?
324
- nil
325
- elsif !from_value
326
- ['%<search_sql>s <= ?', to_value]
327
- elsif !to_value
328
- ['%<search_sql>s >= ?', from_value]
383
+ def datetime_now
384
+ Time.zone.now
385
+ end
386
+
387
+ def datetime_from_to_for_trend(column, value)
388
+ case value['opt']
389
+ when 'PAST'
390
+ trend_number = [value['number'].to_i, 1].max
391
+ now = datetime_now
392
+ if datetime_column_date?(column)
393
+ from = now.beginning_of_day.ago(trend_number.send(value['unit'].downcase.singularize.to_sym))
394
+ to = now.end_of_day
395
+ else
396
+ from = now.ago(trend_number.send(value['unit'].downcase.singularize.to_sym))
397
+ to = now
398
+ end
399
+ [from, to]
400
+ when 'FUTURE'
401
+ trend_number = [value['number'].to_i, 1].max
402
+ now = datetime_now
403
+ if datetime_column_date?(column)
404
+ from = now.beginning_of_day
405
+ to = now.end_of_day.in(trend_number.send(value['unit'].downcase.singularize.to_sym))
406
+ else
407
+ from = now
408
+ to = now.in(trend_number.send(value['unit'].downcase.singularize.to_sym))
409
+ end
410
+ [from, to]
411
+ end
412
+ end
413
+
414
+ def datetime_from_to_for_range(column, value)
415
+ case value['range']
416
+ when 'TODAY'
417
+ [datetime_now.beginning_of_day, datetime_now.end_of_day]
418
+ when 'YESTERDAY'
419
+ [datetime_now.ago(1.day).beginning_of_day, datetime_now.ago(1.day).end_of_day]
420
+ when 'TOMORROW'
421
+ [datetime_now.in(1.day).beginning_of_day, datetime_now.in(1.day).end_of_day]
329
422
  else
330
- ['%<search_sql>s BETWEEN ? AND ?', from_value, to_value]
423
+ range_type, range = value['range'].downcase.split('_')
424
+ raise ArgumentError unless %w[week month year].include?(range)
425
+ case range_type
426
+ when 'this'
427
+ return datetime_now.send("beginning_of_#{range}".to_sym), datetime_now.send("end_of_#{range}")
428
+ when 'prev'
429
+ return datetime_now.ago(1.send(range.to_sym)).send("beginning_of_#{range}".to_sym), datetime_now.ago(1.send(range.to_sym)).send("end_of_#{range}".to_sym)
430
+ when 'next'
431
+ return datetime_now.in(1.send(range.to_sym)).send("beginning_of_#{range}".to_sym), datetime_now.in(1.send(range.to_sym)).send("end_of_#{range}".to_sym)
432
+ else
433
+ return nil, nil
434
+ end
331
435
  end
332
436
  end
333
437
 
438
+ def datetime_column_date?(column)
439
+ column.column&.type == :date
440
+ end
441
+
334
442
  def condition_for_record_select_type(column, value, like_pattern = nil)
335
443
  if value.is_a?(Array)
336
444
  ['%<search_sql>s IN (?)', value]
@@ -361,9 +469,16 @@ module ActiveScaffold
361
469
  STRING_COMPARATORS = {
362
470
  :contains => '%?%',
363
471
  :begins_with => '?%',
364
- :ends_with => '%?'
472
+ :ends_with => '%?',
473
+ :doesnt_contain => 'not_%?%',
474
+ :doesnt_begin_with => 'not_?%',
475
+ :doesnt_end_with => 'not_%?'
365
476
  }.freeze
366
477
  NULL_COMPARATORS = %w[null not_null].freeze
478
+ DATE_COMPARATORS = %w[PAST FUTURE RANGE].freeze
479
+ DATE_UNITS = %w[DAYS WEEKS MONTHS YEARS].freeze
480
+ TIME_UNITS = %w[SECONDS MINUTES HOURS].freeze
481
+ DATE_RANGES = %w[TODAY YESTERDAY TOMORROW THIS_WEEK PREV_WEEK NEXT_WEEK THIS_MONTH PREV_MONTH NEXT_MONTH THIS_YEAR PREV_YEAR NEXT_YEAR].freeze
367
482
 
368
483
  def self.included(klass)
369
484
  klass.extend ClassMethods
@@ -25,17 +25,22 @@ module ActiveScaffold
25
25
  end
26
26
 
27
27
  def generate_temporary_id(record = nil, generated_id = nil)
28
- (generated_id || (Time.now.to_f * 1000).to_i.to_s).tap do |id|
29
- cache_generated_id record, id
28
+ unless generated_id
29
+ temp_id = (Time.now.to_f * 1_000_000).to_i.to_s
30
+ temp_id.succ! while @temporary_ids&.dig(record.class.name)&.include?(temp_id)
31
+ generated_id = temp_id
30
32
  end
33
+ cache_generated_id record, generated_id
34
+ generated_id
31
35
  end
32
36
 
33
37
  def cache_generated_id(record, generated_id)
34
- (@temporary_ids ||= {})[record.class.name] = generated_id if record && generated_id
38
+ # cache all generated ids for the same class, so generate_temporary_id can check and ensure ids are unique
39
+ ((@temporary_ids ||= {})[record.class.name] ||= []) << generated_id if record && generated_id
35
40
  end
36
41
 
37
42
  def generated_id(record)
38
- @temporary_ids[record.class.name] if record && @temporary_ids
43
+ @temporary_ids[record.class.name]&.last if record && @temporary_ids
39
44
  end
40
45
 
41
46
  # These params should not propagate: