active_scaffold 3.6.20 → 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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +47 -0
  3. data/README.md +29 -16
  4. data/app/assets/javascripts/jquery/active_scaffold.js +106 -68
  5. data/app/assets/javascripts/jquery/active_scaffold_chosen.js +6 -5
  6. data/app/assets/javascripts/jquery/tiny_mce_bridge.js +18 -4
  7. data/app/assets/stylesheets/active_scaffold_layout.css +13 -2
  8. data/app/views/active_scaffold_overrides/_base_form.html.erb +5 -1
  9. data/app/views/active_scaffold_overrides/_field_search.html.erb +1 -0
  10. data/app/views/active_scaffold_overrides/_form_association_record.html.erb +2 -1
  11. data/app/views/active_scaffold_overrides/_render_field.js.erb +24 -13
  12. data/config/locales/de.yml +6 -3
  13. data/config/locales/en.yml +3 -0
  14. data/config/locales/es.yml +3 -0
  15. data/config/locales/fr.yml +9 -6
  16. data/config/locales/hu.yml +20 -17
  17. data/config/locales/ja.yml +83 -80
  18. data/config/locales/ru.yml +17 -14
  19. data/lib/active_scaffold/actions/common_search.rb +2 -2
  20. data/lib/active_scaffold/actions/core.rb +30 -10
  21. data/lib/active_scaffold/actions/field_search.rb +9 -6
  22. data/lib/active_scaffold/actions/nested.rb +7 -7
  23. data/lib/active_scaffold/actions/update.rb +3 -3
  24. data/lib/active_scaffold/attribute_params.rb +22 -70
  25. data/lib/active_scaffold/bridges/active_storage/active_storage_helpers.rb +0 -3
  26. data/lib/active_scaffold/bridges/active_storage/form_ui.rb +6 -6
  27. data/lib/active_scaffold/bridges/active_storage/list_ui.rb +7 -7
  28. data/lib/active_scaffold/bridges/active_storage.rb +3 -0
  29. data/lib/active_scaffold/bridges/ancestry/ancestry_bridge.rb +2 -2
  30. data/lib/active_scaffold/bridges/bitfields/bitfields_bridge.rb +2 -2
  31. data/lib/active_scaffold/bridges/calendar_date_select/as_cds_bridge.rb +12 -14
  32. data/lib/active_scaffold/bridges/carrierwave/form_ui.rb +2 -2
  33. data/lib/active_scaffold/bridges/carrierwave/list_ui.rb +1 -1
  34. data/lib/active_scaffold/bridges/chosen/helpers.rb +10 -10
  35. data/lib/active_scaffold/bridges/country_select/country_select_bridge_helper.rb +7 -7
  36. data/lib/active_scaffold/bridges/date_picker/ext.rb +20 -9
  37. data/lib/active_scaffold/bridges/date_picker/helper.rb +9 -9
  38. data/lib/active_scaffold/bridges/date_picker.rb +2 -0
  39. data/lib/active_scaffold/bridges/dragonfly/form_ui.rb +3 -3
  40. data/lib/active_scaffold/bridges/dragonfly/list_ui.rb +5 -5
  41. data/lib/active_scaffold/bridges/file_column/form_ui.rb +1 -1
  42. data/lib/active_scaffold/bridges/file_column/list_ui.rb +3 -3
  43. data/lib/active_scaffold/bridges/file_column/test/functional/file_column_keep_test.rb +1 -1
  44. data/lib/active_scaffold/bridges/paper_trail/actions.rb +4 -1
  45. data/lib/active_scaffold/bridges/paperclip/form_ui.rb +3 -3
  46. data/lib/active_scaffold/bridges/paperclip/list_ui.rb +1 -1
  47. data/lib/active_scaffold/bridges/record_select/helpers.rb +17 -17
  48. data/lib/active_scaffold/bridges/tiny_mce/helpers.rb +6 -6
  49. data/lib/active_scaffold/bridges/tiny_mce.rb +1 -1
  50. data/lib/active_scaffold/bridges/usa_state_select/usa_state_select_helper.rb +6 -11
  51. data/lib/active_scaffold/bridges.rb +0 -3
  52. data/lib/active_scaffold/config/core.rb +1 -1
  53. data/lib/active_scaffold/config/field_search.rb +9 -1
  54. data/lib/active_scaffold/config/form.rb +9 -1
  55. data/lib/active_scaffold/constraints.rb +22 -7
  56. data/lib/active_scaffold/core.rb +6 -10
  57. data/lib/active_scaffold/data_structures/action_columns.rb +0 -25
  58. data/lib/active_scaffold/data_structures/action_links.rb +1 -1
  59. data/lib/active_scaffold/data_structures/association/abstract.rb +8 -0
  60. data/lib/active_scaffold/data_structures/association/active_mongoid.rb +8 -0
  61. data/lib/active_scaffold/data_structures/association/active_record.rb +1 -13
  62. data/lib/active_scaffold/data_structures/association/mongoid.rb +21 -8
  63. data/lib/active_scaffold/data_structures/column.rb +139 -28
  64. data/lib/active_scaffold/data_structures/columns.rb +12 -12
  65. data/lib/active_scaffold/data_structures/nested_info.rb +12 -0
  66. data/lib/active_scaffold/data_structures/sorting.rb +1 -1
  67. data/lib/active_scaffold/engine.rb +15 -1
  68. data/lib/active_scaffold/extensions/action_view_rendering.rb +13 -5
  69. data/lib/active_scaffold/extensions/cow_proxy.rb +1 -1
  70. data/lib/active_scaffold/extensions/routing_mapper.rb +1 -0
  71. data/lib/active_scaffold/extensions/unsaved_record.rb +9 -3
  72. data/lib/active_scaffold/finder.rb +147 -28
  73. data/lib/active_scaffold/helpers/action_link_helpers.rb +1 -1
  74. data/lib/active_scaffold/helpers/controller_helpers.rb +9 -4
  75. data/lib/active_scaffold/helpers/form_column_helpers.rb +153 -107
  76. data/lib/active_scaffold/helpers/human_condition_helpers.rb +48 -14
  77. data/lib/active_scaffold/helpers/list_column_helpers.rb +37 -20
  78. data/lib/active_scaffold/helpers/search_column_helpers.rb +137 -55
  79. data/lib/active_scaffold/helpers/show_column_helpers.rb +6 -6
  80. data/lib/active_scaffold/helpers/view_helpers.rb +1 -1
  81. data/lib/active_scaffold/orm_checks.rb +21 -1
  82. data/lib/active_scaffold/registry.rb +10 -15
  83. data/lib/active_scaffold/tableless.rb +10 -79
  84. data/lib/active_scaffold/version.rb +2 -2
  85. data/lib/active_scaffold.rb +3 -9
  86. data/lib/generators/active_scaffold/install_generator.rb +2 -2
  87. data/test/bridges/bridge_test.rb +1 -1
  88. data/test/bridges/date_picker_test.rb +3 -2
  89. data/test/bridges/paperclip_test.rb +18 -14
  90. data/test/bridges/tiny_mce_test.rb +5 -3
  91. data/test/config/base_test.rb +1 -1
  92. data/test/config/core_test.rb +1 -1
  93. data/test/config/create_test.rb +1 -1
  94. data/test/config/delete_test.rb +1 -1
  95. data/test/config/field_search_test.rb +1 -1
  96. data/test/config/list_test.rb +1 -1
  97. data/test/config/nested_test.rb +1 -1
  98. data/test/config/search_test.rb +1 -1
  99. data/test/config/show_test.rb +1 -1
  100. data/test/config/subform_test.rb +1 -1
  101. data/test/config/update_test.rb +1 -1
  102. data/test/data_structures/action_columns_test.rb +1 -1
  103. data/test/data_structures/action_link_test.rb +1 -1
  104. data/test/data_structures/action_links_test.rb +1 -1
  105. data/test/data_structures/actions_test.rb +1 -1
  106. data/test/data_structures/association_column_test.rb +1 -1
  107. data/test/data_structures/column_test.rb +1 -1
  108. data/test/data_structures/columns_test.rb +1 -1
  109. data/test/data_structures/set_test.rb +1 -1
  110. data/test/data_structures/sorting_test.rb +1 -1
  111. data/test/data_structures/standard_column_test.rb +1 -1
  112. data/test/data_structures/validation_reflection_test.rb +1 -1
  113. data/test/data_structures/virtual_column_test.rb +1 -1
  114. data/test/extensions/active_record_test.rb +1 -1
  115. data/test/helpers/form_column_helpers_test.rb +7 -5
  116. data/test/helpers/pagination_helpers_test.rb +1 -1
  117. data/test/helpers/search_column_helpers_test.rb +2 -1
  118. data/test/misc/active_record_permissions_test.rb +1 -1
  119. data/test/misc/attribute_params_test.rb +1 -1
  120. data/test/misc/calculation_test.rb +1 -1
  121. data/test/misc/configurable_test.rb +1 -1
  122. data/test/misc/constraints_test.rb +2 -1
  123. data/test/misc/convert_numbers_format_test.rb +1 -1
  124. data/test/misc/finder_test.rb +39 -1
  125. data/test/misc/lang_test.rb +1 -1
  126. data/test/misc/parse_datetime_test.rb +1 -1
  127. data/test/misc/tableless_test.rb +1 -1
  128. data/test/test_helper.rb +4 -4
  129. metadata +5 -17
  130. data/config/brakeman.ignore +0 -26
  131. data/config/brakeman.yml +0 -3
  132. data/config/i18n-tasks.yml +0 -121
  133. data/lib/active_scaffold/bridges/shared/date_bridge.rb +0 -221
  134. data/lib/active_scaffold/delayed_setup.rb +0 -41
  135. data/lib/active_scaffold/extensions/left_outer_joins.rb +0 -43
@@ -110,26 +110,57 @@ 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
125
- ['%<search_sql>s = ?', column.column ? ActiveScaffold::Core.column_type_cast(value, column.column) : value]
152
+ if value == 'null'
153
+ condition_for_null_type(column, value)
154
+ else
155
+ ['%<search_sql>s = ?', column.column ? ActiveScaffold::Core.column_type_cast(value, column.column) : value]
156
+ end
126
157
  when :integer, :decimal, :float
127
158
  condition_for_numeric(column, value)
128
159
  when :string, :range
129
160
  condition_for_range(column, value, like_pattern)
130
161
  when :date, :time, :datetime, :timestamp
131
162
  condition_for_datetime(column, value)
132
- when :select, :multi_select, :country, :usa_state, :chosen, :multi_chosen
163
+ when :select, :select_multiple, :draggable, :multi_select, :country, :usa_state, :chosen, :multi_chosen
133
164
  values = Array(value).select(&:present?)
134
165
  ['%<search_sql>s in (?)', values] if values.present?
135
166
  else
@@ -167,7 +198,10 @@ module ActiveScaffold
167
198
  elsif value[:from].blank?
168
199
  nil
169
200
  elsif ActiveScaffold::Finder::STRING_COMPARATORS.values.include?(value[:opt])
170
- ["%<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
+ ]
171
205
  elsif value[:opt] == 'BETWEEN'
172
206
  ['(%<search_sql>s BETWEEN ? AND ?)', value[:from], value[:to]]
173
207
  elsif ActiveScaffold::Finder::NUMERIC_COMPARATORS.include?(value[:opt])
@@ -237,14 +271,19 @@ module ActiveScaffold
237
271
  nil
238
272
  end
239
273
 
240
- def parse_date_with_format(value, format_name)
241
- format = I18n.t("date.formats.#{format_name || :default}")
242
- format.gsub!(/%-d|%-m|%_m/) { |s| s.gsub(/[-_]/, '') } # strptime fails with %-d, %-m, %_m
243
- en_value = I18n.locale == :en ? value : translate_days_and_months(value, format)
244
- 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)
245
285
  rescue StandardError => e
246
- message = "Error parsing date from #{en_value}"
247
- message << " (#{value})" if en_value != value
286
+ message = "Error parsing date from #{value}"
248
287
  message << ", with format #{format}" if format
249
288
  Rails.logger.warn "#{message}:\n#{e.message}\n#{e.backtrace.join("\n")}"
250
289
  nil
@@ -276,7 +315,7 @@ module ActiveScaffold
276
315
  value.send(conversion)
277
316
  end
278
317
  elsif conversion == :to_date
279
- parse_date_with_format(value, column.options[:format])
318
+ parse_date_with_format(*format_for_date(column, value))
280
319
  elsif value.include?('T')
281
320
  Time.zone.parse(value)
282
321
  else # datetime
@@ -288,7 +327,7 @@ module ActiveScaffold
288
327
  def condition_value_for_numeric(column, value)
289
328
  return value if value.nil?
290
329
  value = column.number_to_native(value) if column.options[:format] && column.search_ui != :number
291
- case (column.search_ui || column.column.type)
330
+ case (column.search_ui || column.column_type)
292
331
  when :integer then
293
332
  if value.is_a?(TrueClass) || value.is_a?(FalseClass)
294
333
  value ? 1 : 0
@@ -296,8 +335,7 @@ module ActiveScaffold
296
335
  value.to_i
297
336
  end
298
337
  when :float then value.to_f
299
- when :decimal
300
- ::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)
301
339
  else
302
340
  value
303
341
  end
@@ -305,28 +343,102 @@ module ActiveScaffold
305
343
 
306
344
  def datetime_conversion_for_condition(column)
307
345
  if column.column
308
- column.column.type == :date ? :to_date : :to_time
346
+ column.column_type == :date ? :to_date : :to_time
309
347
  else
310
348
  :to_time
311
349
  end
312
350
  end
313
351
 
314
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)
315
366
  conversion = datetime_conversion_for_condition(column)
316
- from_value = condition_value_for_datetime(column, value[:from], conversion)
317
- 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
318
382
 
319
- if from_value.nil? && to_value.nil?
320
- nil
321
- elsif !from_value
322
- ['%<search_sql>s <= ?', to_value]
323
- elsif !to_value
324
- ['%<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]
325
422
  else
326
- ['%<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
327
435
  end
328
436
  end
329
437
 
438
+ def datetime_column_date?(column)
439
+ column.column&.type == :date
440
+ end
441
+
330
442
  def condition_for_record_select_type(column, value, like_pattern = nil)
331
443
  if value.is_a?(Array)
332
444
  ['%<search_sql>s IN (?)', value]
@@ -357,9 +469,16 @@ module ActiveScaffold
357
469
  STRING_COMPARATORS = {
358
470
  :contains => '%?%',
359
471
  :begins_with => '?%',
360
- :ends_with => '%?'
472
+ :ends_with => '%?',
473
+ :doesnt_contain => 'not_%?%',
474
+ :doesnt_begin_with => 'not_?%',
475
+ :doesnt_end_with => 'not_%?'
361
476
  }.freeze
362
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
363
482
 
364
483
  def self.included(klass)
365
484
  klass.extend ClassMethods
@@ -212,7 +212,7 @@ module ActiveScaffold
212
212
  end
213
213
 
214
214
  def column_in_params_conditions?(key)
215
- if key =~ /!$/
215
+ if key.match?(/!$/)
216
216
  conditions_from_params[1..-1].any? { |node| node.left.name.to_s == key[0..-2] }
217
217
  else
218
218
  conditions_from_params[0].include?(key)
@@ -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: