active_scaffold 3.6.20 → 3.7.1

Sign up to get free protection for your applications and to get access to all the features.
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: