active_scaffold 3.7.0 → 3.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +24 -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 +19 -1
  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 +4 -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,22 +2,33 @@ 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'
5
7
  include ActiveScaffold::Core
6
8
  include ActiveScaffold::RespondsToParent
7
9
  include ActiveScaffold::Helpers::ControllerHelpers
8
10
  include ActiveScaffold::ActiveRecordPermissions::ModelUserAccess::Controller
9
- ActiveScaffold::Bridges.prepare_all
10
11
  end
11
12
  end
12
13
 
14
+ config.after_initialize do
15
+ require 'active_scaffold/extensions/routing_mapper'
16
+ ActiveScaffold::Bridges.prepare_all
17
+ end
18
+
13
19
  initializer 'active_scaffold.action_view' do
14
20
  ActiveSupport.on_load :action_view do
21
+ require 'active_scaffold/extensions/action_view_rendering'
22
+ require 'active_scaffold/extensions/name_option_for_datetime'
15
23
  include ActiveScaffold::Helpers::ViewHelpers
16
24
  end
17
25
  end
18
26
 
19
27
  initializer 'active_scaffold.active_record' do
20
28
  ActiveSupport.on_load :active_record do
29
+ require 'active_scaffold/extensions/to_label'
30
+ require 'active_scaffold/extensions/unsaved_associated'
31
+ require 'active_scaffold/extensions/unsaved_record'
21
32
  include ActiveScaffold::ActiveRecordPermissions::ModelUserAccess::Model
22
33
  module ActiveRecord::Associations
23
34
  Association.send :include, ActiveScaffold::Tableless::Association
@@ -39,5 +50,12 @@ module ActiveScaffold
39
50
  initializer 'active_scaffold.assets' do
40
51
  config.assets.precompile << 'active_scaffold/indicator.gif'
41
52
  end
53
+
54
+ initializer 'active_scaffold.extensions' do
55
+ require 'active_scaffold/extensions/cow_proxy'
56
+ require 'active_scaffold/extensions/ice_nine'
57
+ require 'active_scaffold/extensions/localize'
58
+ require 'active_scaffold/extensions/paginator_extensions'
59
+ end
42
60
  end
43
61
  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: