active_scaffold 4.3.2 → 4.4.0

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +19 -0
  3. data/README.md +1 -1
  4. data/app/assets/javascripts/active_scaffold/active_scaffold.js +98 -11
  5. data/app/assets/javascripts/active_scaffold/draggable_lists.js +4 -3
  6. data/app/assets/javascripts/active_scaffold/load.js +20 -5
  7. data/app/assets/stylesheets/active_scaffold/_images.scss +53 -1
  8. data/app/assets/stylesheets/active_scaffold/_layout.scss +1 -66
  9. data/app/views/active_scaffold_overrides/_add_existing_form.html.erb +17 -11
  10. data/app/views/active_scaffold_overrides/_base_form.html.erb +29 -19
  11. data/app/views/active_scaffold_overrides/_field_search.html.erb +9 -9
  12. data/app/views/active_scaffold_overrides/_field_search_columns.html.erb +29 -33
  13. data/app/views/active_scaffold_overrides/_form.html.erb +43 -34
  14. data/app/views/active_scaffold_overrides/_form_association.html.erb +3 -3
  15. data/app/views/active_scaffold_overrides/_form_association_footer.html.erb +20 -16
  16. data/app/views/active_scaffold_overrides/_form_association_record.html.erb +3 -4
  17. data/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb +2 -1
  18. data/app/views/active_scaffold_overrides/_list.html.erb +6 -4
  19. data/app/views/active_scaffold_overrides/_list_calculations.html.erb +1 -1
  20. data/app/views/active_scaffold_overrides/_list_header.html.erb +3 -3
  21. data/app/views/active_scaffold_overrides/_list_messages.html.erb +19 -19
  22. data/app/views/active_scaffold_overrides/_list_messages_content.html.erb +5 -5
  23. data/app/views/active_scaffold_overrides/_list_pagination.html.erb +7 -5
  24. data/app/views/active_scaffold_overrides/_list_pagination_links.html.erb +5 -3
  25. data/app/views/active_scaffold_overrides/_list_record.html.erb +13 -8
  26. data/app/views/active_scaffold_overrides/_list_with_header.html.erb +8 -8
  27. data/app/views/active_scaffold_overrides/_messages.html.erb +3 -3
  28. data/app/views/active_scaffold_overrides/_render_field.js.erb +8 -11
  29. data/app/views/active_scaffold_overrides/_search.html.erb +7 -6
  30. data/app/views/active_scaffold_overrides/_show.html.erb +4 -4
  31. data/app/views/active_scaffold_overrides/_show_actions.html.erb +2 -2
  32. data/app/views/active_scaffold_overrides/_show_columns.html.erb +28 -30
  33. data/app/views/active_scaffold_overrides/add_existing.js.erb +7 -4
  34. data/app/views/active_scaffold_overrides/render_field.js.erb +1 -1
  35. data/config/locales/de.yml +2 -1
  36. data/config/locales/en.yml +33 -32
  37. data/config/locales/es.yml +15 -14
  38. data/config/locales/fr.yml +3 -2
  39. data/config/locales/hu.yml +3 -2
  40. data/config/locales/ja.yml +2 -1
  41. data/config/locales/ro.yml +169 -0
  42. data/config/locales/ru.yml +8 -7
  43. data/config/locales/ta.yml +7 -6
  44. data/config/locales/zh_Hans.yml +14 -13
  45. data/lib/active_scaffold/actions/common_search.rb +1 -14
  46. data/lib/active_scaffold/actions/core.rb +55 -16
  47. data/lib/active_scaffold/actions/field_search.rb +2 -2
  48. data/lib/active_scaffold/actions/list.rb +35 -13
  49. data/lib/active_scaffold/actions/nested.rb +31 -7
  50. data/lib/active_scaffold/actions/show.rb +1 -1
  51. data/lib/active_scaffold/active_record_permissions.rb +1 -1
  52. data/lib/active_scaffold/attribute_params.rb +2 -1
  53. data/lib/active_scaffold/bridges/date_picker/helper.rb +14 -0
  54. data/lib/active_scaffold/bridges/date_picker.rb +10 -0
  55. data/lib/active_scaffold/config/list.rb +10 -1
  56. data/lib/active_scaffold/core.rb +9 -6
  57. data/lib/active_scaffold/data_structures/action_columns.rb +2 -0
  58. data/lib/active_scaffold/data_structures/column.rb +27 -6
  59. data/lib/active_scaffold/data_structures/filter.rb +4 -0
  60. data/lib/active_scaffold/data_structures/nested_info.rb +8 -1
  61. data/lib/active_scaffold/data_structures/sorting.rb +1 -1
  62. data/lib/active_scaffold/extensions/action_view_rendering.rb +1 -1
  63. data/lib/active_scaffold/finder.rb +66 -21
  64. data/lib/active_scaffold/helpers/action_link_helpers.rb +43 -18
  65. data/lib/active_scaffold/helpers/assets_helpers.rb +4 -4
  66. data/lib/active_scaffold/helpers/filter_helpers.rb +3 -3
  67. data/lib/active_scaffold/helpers/form_column_helpers.rb +34 -22
  68. data/lib/active_scaffold/helpers/form_ui_helpers.rb +15 -10
  69. data/lib/active_scaffold/helpers/framework_ui_helpers.rb +22 -0
  70. data/lib/active_scaffold/helpers/human_condition_helpers.rb +1 -0
  71. data/lib/active_scaffold/helpers/list_column_helpers.rb +19 -0
  72. data/lib/active_scaffold/helpers/pagination_helpers.rb +28 -7
  73. data/lib/active_scaffold/helpers/search_column_helpers.rb +32 -5
  74. data/lib/active_scaffold/helpers/tabs_helpers.rb +3 -5
  75. data/lib/active_scaffold/helpers/view_helpers.rb +12 -0
  76. data/lib/active_scaffold/tableless.rb +11 -8
  77. data/lib/active_scaffold/version.rb +2 -2
  78. data/lib/active_scaffold.rb +169 -0
  79. data/lib/tasks/bundle.rake +2 -1
  80. data/lib/tasks/mock_app.rake +43 -0
  81. metadata +20 -7
  82. data/shoulda_macros/macros.rb +0 -145
@@ -20,7 +20,7 @@ ta:
20
20
  begins_with: 'உடன் தொடங்குகிறது'
21
21
  between: 'இடையில்'
22
22
  cancel: 'ரத்துசெய்'
23
- cant_destroy_record: "%{record} அழிக்க முடியாது"
23
+ cant_destroy_record: "%{record} அழிக்க முடியாது."
24
24
  changes: 'மாற்றங்கள்'
25
25
  check_all: 'அனைத்தையும் சரிபார்க்கவும்'
26
26
  click_to_edit: 'திருத்த சொடுக்கு செய்யவும்'
@@ -38,8 +38,8 @@ ta:
38
38
  customize: 'தனிப்பயனாக்கு'
39
39
  date_picker_options:
40
40
  firstDay: '0'
41
- isRTL: 'பொய்'
42
- showMonthAfterYear: 'பொய்'
41
+ isRTL: 'false'
42
+ showMonthAfterYear: 'false'
43
43
  weekHeader: 'Wk'
44
44
  datetime_picker_options:
45
45
  closeText: 'மூடு'
@@ -63,7 +63,7 @@ ta:
63
63
  body: 'பின்வரும் துறைகளில் சிக்கல்கள் உள்ளன:'
64
64
  header:
65
65
  one: 1 பிழை இந்த %{model} சேமிக்கப்படுவதைத் தடுக்கிறது.
66
- other: '%{count} பிழைகள் இந்த %{model} சேமிக்கப்படுவதைத் தடுக்கின்றன'
66
+ other: '%{count} பிழைகள் இந்த %{model} சேமிக்கப்படுவதைத் தடுக்கின்றன.'
67
67
  export: 'ஏற்றுமதி'
68
68
  'false': 'பொய்'
69
69
  filtered: "(வடிகட்டப்பட்டது)"
@@ -92,7 +92,7 @@ ta:
92
92
  next_month: 'அடுத்த மாதம்'
93
93
  next_week: 'அடுத்த வாரம்'
94
94
  next_year: 'அடுத்த வருடம்'
95
- no_authorization_for_action: 'செயலுக்கு ஏற்பு இல்லை %{action}'
95
+ no_authorization_for_action: 'செயலுக்கு ஏற்பு இல்லை %{action}.'
96
96
  no_color: 'நிறம் இல்லை'
97
97
  no_entries: 'உள்ளீடுகள் இல்லை'
98
98
  no_group: 'குழு இல்லை'
@@ -110,7 +110,7 @@ ta:
110
110
  previous: 'முந்தைய'
111
111
  print: 'அச்சிடுக'
112
112
  range: 'வீச்சு'
113
- record_not_saved: 'அறியப்படாத பிழைக்கான பதிவைச் சேமிப்பதில் தோல்வி'
113
+ record_not_saved: 'அறியப்படாத பிழைக்கான பதிவைச் சேமிப்பதில் தோல்வி.'
114
114
  records_marked:
115
115
  one: 1 குறிக்கப்பட்டது %{model}
116
116
  other: '%{count} %{model} எனக் குறிக்கப்பட்டது'
@@ -146,6 +146,7 @@ ta:
146
146
  weeks: 'வாரங்கள்'
147
147
  years: 'ஆண்டுகள்'
148
148
  yesterday: 'நேற்று'
149
+ already_added_model: '%{model} has already been added.'
149
150
  date:
150
151
  formats:
151
152
  month: "%B"
@@ -20,7 +20,7 @@ zh_Hans:
20
20
  begins_with: 以……开始
21
21
  between: 介于……之间
22
22
  cancel: 取消
23
- cant_destroy_record: '%{record}不能被删除'
23
+ cant_destroy_record: '%{record}不能被删除。'
24
24
  changes: 变更记录
25
25
  check_all: 全选
26
26
  click_to_edit: 点击以编辑
@@ -30,7 +30,7 @@ zh_Hans:
30
30
  config_list_model: 为%{model}配置列
31
31
  contains: 包含
32
32
  create: 创建
33
- create_another: 新建 %{model}
33
+ create_another: 新建另一 %{model}
34
34
  create_apply: 创建并继续
35
35
  create_model: 创建 %{model}
36
36
  create_new: 新建
@@ -62,15 +62,15 @@ zh_Hans:
62
62
  template:
63
63
  body: 以下字段存在问题:
64
64
  header:
65
- other: '%{count} 个错误阻止了这个 %{model} 被保存'
66
- export: Export
67
- 'false': 'False'
65
+ other: '%{count} 个错误阻止了该 %{model} 的保存。'
66
+ export: 导出
67
+ 'false': '假'
68
68
  filtered: (Filtered)
69
69
  found: 发现了
70
70
  future: Future
71
71
  group_by: Group by
72
72
  grouped_by: Grouped by %{column}
73
- hide: (Hide)
73
+ hide: (隐藏)
74
74
  hours: Hours
75
75
  human_conditions:
76
76
  association: '%{column} = %{value}'
@@ -81,9 +81,9 @@ zh_Hans:
81
81
  loading: Loading…
82
82
  logical_search: Logical Search
83
83
  mark_all_records: Mark all
84
- maximum: Maximum
85
- minimum: Minimum
86
- minutes: Minutes
84
+ maximum: 最大值
85
+ minimum: 最小值
86
+ minutes: 分钟
87
87
  months: 月
88
88
  nested_for_model: '%{nested_model} for %{parent_model}'
89
89
  nested_of_model: '%{nested_model} of %{parent_model}'
@@ -92,8 +92,8 @@ zh_Hans:
92
92
  next_week: Next Week
93
93
  next_year: Next Year
94
94
  no_authorization_for_action: No Authorization for action %{action}
95
- no_color: No color
96
- no_entries: No Entries
95
+ no_color: 无颜色
96
+ no_entries: 无条目
97
97
  no_group: No group
98
98
  no_options: no options
99
99
  'null': 'Null'
@@ -104,8 +104,8 @@ zh_Hans:
104
104
  pdf: PDF
105
105
  prev_month: Last Month
106
106
  prev_week: Last Week
107
- prev_year: Last Year
108
- previous: Previous
107
+ prev_year: 去年
108
+ previous: 上一个
109
109
  print: Print
110
110
  range: Range
111
111
  record_not_saved: Failed to save record cause of an unknown error
@@ -144,6 +144,7 @@ zh_Hans:
144
144
  years: Years
145
145
  yesterday: Yesterday
146
146
  not_null: Not Null
147
+ already_added_model: '%{model} has already been added.'
147
148
  date:
148
149
  formats:
149
150
  month: '%B'
@@ -35,20 +35,7 @@ module ActiveScaffold::Actions
35
35
  end
36
36
 
37
37
  def set_outer_joins_for_search(columns) # rubocop:disable Naming/AccessorMethodName
38
- references = []
39
- outer_joins = []
40
- columns.each do |column|
41
- next if column.search_joins.blank?
42
-
43
- if column.includes.present? && list_columns.include?(column)
44
- references << (column.search_joins & column.includes)
45
- outer_joins << (column.search_joins - column.includes)
46
- else
47
- outer_joins << column.search_joins
48
- end
49
- end
50
- active_scaffold_references.concat references.flatten.uniq.compact
51
- active_scaffold_outer_joins.concat outer_joins.flatten.uniq.compact
38
+ active_scaffold_outer_joins.concat columns.flat_map(&:search_joins).uniq
52
39
  end
53
40
 
54
41
  def store_search_params_into_session
@@ -366,34 +366,29 @@ module ActiveScaffold::Actions
366
366
  active_scaffold_config.model
367
367
  end
368
368
 
369
- # Builds search conditions by search params for column names. This allows urls like "contacts/list?company_id=5".
369
+ # Builds search conditions from query parameters whose names match columns.
370
+ # For example, `contacts/list?company_id=5` matches records with company_id 5.
371
+ # Append `!` to a column name to negate the condition, as in `company_id!=5`.
372
+ # A value containing exactly one `..` defines a range for supported column types, for example
373
+ # `created_at=2025-01-01..2025-12-31`. A missing range endpoint creates an infinite range,
374
+ # such as `created_at=..2025-12-31` or `created_at=2025-01-01..`.
375
+ # Array parameters use the `column[]=value` syntax, for example `company_id[]=5&company_id[]=10`.
376
+ # Negation may also be combined with ranges or arrays, using `not_between` or `not_in`.
370
377
  def conditions_from_params
371
378
  @conditions_from_params ||= begin
372
379
  conditions = [{}]
373
- supporting_range = %i[date datetime integer decimal float bigint]
374
380
  params.except(:controller, :action, :page, :sort, :sort_direction, :format, :id).each do |key, value|
375
- distinct = true if key.match?(/!$/)
381
+ distinct = key.match?(/!$/)
376
382
  column = active_scaffold_config._columns_hash[key.to_s[0..(distinct ? -2 : -1)]]
377
383
  next unless column
378
384
 
379
385
  key = column.name.to_sym
380
- not_string = %i[string text].exclude?(column.type)
381
386
  next if active_scaffold_constraints[key]
382
387
  next if nested? && nested.param_name == key
383
388
 
384
- range = supporting_range.include?(column.type) && value.is_a?(String) && value.scan('..').size == 1
385
- value = value.split('..') if range
386
- value =
387
- if value.is_a?(Array)
388
- value.map { |v| v == '' && not_string ? nil : ActiveScaffold::Core.column_type_cast(v, column) }
389
- elsif value == '' && (not_string || column.null)
390
- ActiveScaffold::Core.column_type_cast(column.default, column)
391
- else
392
- ActiveScaffold::Core.column_type_cast(value, column)
393
- end
394
- value = Range.new(*value) if range
389
+ value = condition_value_from_param(value, column, active_scaffold_config.model)
395
390
  if distinct
396
- conditions << active_scaffold_config.model.arel_table[key].not_eq(value)
391
+ conditions << distinct_condition(key, value)
397
392
  else
398
393
  conditions[0][key] = value
399
394
  end
@@ -402,6 +397,46 @@ module ActiveScaffold::Actions
402
397
  end
403
398
  end
404
399
 
400
+ def distinct_condition(key, value)
401
+ attribute = active_scaffold_config.model.arel_table[key]
402
+ case value
403
+ when Array
404
+ attribute.not_in(value)
405
+ when Range
406
+ attribute.not_between(value)
407
+ else
408
+ attribute.not_eq(value)
409
+ end
410
+ end
411
+
412
+ def condition_value_from_param(value, column, model)
413
+ range = range_from_param(value, column)
414
+ value = range.captures if range
415
+ not_string = %i[string text].exclude?(column.type)
416
+ value =
417
+ if value.is_a?(Array)
418
+ value.map { |item| conditional_cast_for_column(item, column, not_string, model) }
419
+ elsif value == '' && (not_string || column.null)
420
+ ActiveScaffold::Core.column_type_cast(column.default, column, model)
421
+ else
422
+ ActiveScaffold::Core.column_type_cast(value, column, model)
423
+ end
424
+ range ? Range.new(*value) : value
425
+ end
426
+
427
+ def conditional_cast_for_column(value, column, not_string, model)
428
+ return if value == '' && not_string
429
+
430
+ ActiveScaffold::Core.column_type_cast(value, column, model)
431
+ end
432
+
433
+ def range_from_param(value, column)
434
+ supporting_range = %i[date datetime timestamp timestamptz integer decimal float bigint]
435
+ return unless supporting_range.include?(column.type) && value.is_a?(String) && value.scan('..').size == 1
436
+
437
+ value.match(/\A(.*)\.\.(.*)\z/)
438
+ end
439
+
405
440
  def empty_model
406
441
  relation = beginning_of_chain
407
442
  if nested? && nested.plural_association? && nested.match_model?(active_scaffold_config.model)
@@ -437,6 +472,10 @@ module ActiveScaffold::Actions
437
472
  end
438
473
 
439
474
  def get_row(crud_type_or_security_options = :read)
475
+ get_record(crud_type_or_security_options)
476
+ end
477
+
478
+ def get_record(crud_type_or_security_options = :read)
440
479
  klass = filtered_query
441
480
  klass = klass.preload(active_scaffold_preload) unless active_scaffold_config.mongoid?
442
481
  @record = find_if_allowed(params[:id], crud_type_or_security_options, klass)
@@ -110,8 +110,8 @@ module ActiveScaffold::Actions
110
110
  end
111
111
 
112
112
  def grouped_columns_calculations
113
- @grouped_columns_calculations ||= list_columns[1..].each_with_object({}) do |c, h|
114
- h[c.name] = calculation_for_group_search(c)
113
+ @grouped_columns_calculations ||= list_columns[1..].to_h do |c|
114
+ [c.name, calculation_for_group_search(c)]
115
115
  end
116
116
  end
117
117
 
@@ -172,11 +172,13 @@ module ActiveScaffold::Actions
172
172
  end
173
173
 
174
174
  def count_query_for_column(column, records)
175
- if count_on_association_class?(column)
176
- count_query_on_association_class(column, records)
177
- else
178
- count_query_with_join(column, records)
179
- end
175
+ query =
176
+ if count_on_association_class?(column)
177
+ count_query_on_association_class(column, records)
178
+ else
179
+ count_query_with_join(column, records)
180
+ end
181
+ query.unscope(:order)
180
182
  end
181
183
 
182
184
  def count_query_on_association_class(column, records)
@@ -207,8 +209,8 @@ module ActiveScaffold::Actions
207
209
  end
208
210
  group = {_id: "$#{column.association.foreign_key}", count: {'$sum' => 1}}
209
211
  query = column.association.klass.collection.aggregate([{'$match' => matches}, {'$group' => group}])
210
- query.each_with_object({}) do |row, hash|
211
- hash[row['_id']] = row['count']
212
+ query.to_h do |row|
213
+ [row['_id'], row['count']]
212
214
  end
213
215
  end
214
216
 
@@ -281,24 +283,44 @@ module ActiveScaffold::Actions
281
283
  active_scaffold_config.list.refresh_with_header = true
282
284
 
283
285
  active_scaffold_config.list.filters.inject(query) do |q, filter|
284
- next q unless filter.security_method.nil? || send(filter.security_method)
285
-
286
286
  default_option = filter[filter.default_option]
287
- apply_filter q, params[filter.name] ? filter[params[filter.name]] : default_option, default_option
287
+ filter_option = params[filter.name] ? filter[params[filter.name]] : default_option
288
+ unless filter_allowed?(filter)
289
+ track_filter filter_option, default_option, :disallowed if params.key?(filter.name)
290
+ next q
291
+ end
292
+
293
+ apply_filter q, filter_option, default_option
288
294
  end
289
295
  end
290
296
 
297
+ def filter_allowed?(filter)
298
+ !filter.security_method_set? || send(filter.security_method)
299
+ end
300
+
291
301
  def apply_filter(query, filter_option, default_option)
292
- return query if filter_option.nil? || (filter_option.security_method_set? && !send(filter_option.security_method))
302
+ if filter_option.nil? || !filter_allowed?(filter_option)
303
+ track_filter filter_option, default_option, filter_option ? :disallowed : :unknown
304
+ return query
305
+ end
293
306
 
294
- @applied_filters ||= []
295
- @applied_filters << filter_option unless filter_option == default_option
307
+ track_filter filter_option, default_option, :applied
296
308
  case filter_option.conditions
297
309
  when Proc then instance_exec query, &filter_option.conditions
298
310
  else query.where(filter_option.conditions)
299
311
  end
300
312
  end
301
313
 
314
+ def track_filter(filter_option, default_option, status)
315
+ @filter_states ||= {}
316
+ @filter_states[default_option.filter_name] = {
317
+ option: filter_option,
318
+ status: status,
319
+ default: filter_option == default_option,
320
+ requested: params.key?(default_option.filter_name)
321
+ }
322
+ end
323
+
302
324
  def scoped_query
303
325
  @scoped_query ||= begin
304
326
  do_search if respond_to? :do_search, true
@@ -179,7 +179,9 @@ module ActiveScaffold::Actions::Nested
179
179
 
180
180
  def add_existing_respond_to_html
181
181
  if successful?
182
- flash[:info] = as_(:created_model, model: ERB::Util.h(@record.to_label))
182
+ unless @association_already_exists
183
+ flash[:info] = as_(:created_model, model: ERB::Util.h(@record.to_label))
184
+ end
183
185
  return_to_main
184
186
  else
185
187
  render action: 'add_existing_form'
@@ -250,12 +252,34 @@ module ActiveScaffold::Actions::Nested
250
252
  def do_add_existing
251
253
  parent_record = nested_parent_record(:update)
252
254
  @record = active_scaffold_config.model.find(params[:associated_id])
253
- if parent_record && @record
254
- self.successful = false unless parent_record.send(nested.association.name) << @record
255
- parent_record.save if successful?
256
- else
257
- false
258
- end
255
+ return false unless parent_record && @record
256
+
257
+ association = parent_record.send(nested.association.name)
258
+ added = add_existing_to_association(association)
259
+ return true if @association_already_exists
260
+
261
+ self.successful = false unless added
262
+ parent_record.save if successful?
263
+ end
264
+
265
+ def add_existing_to_association(association)
266
+ return association_already_exists if association.include?(@record)
267
+
268
+ association << @record
269
+ rescue ActiveRecord::RecordNotUnique
270
+ # Handles concurrent submissions. Only suppress the exception when the
271
+ # requested association now exists; otherwise it was a different
272
+ # uniqueness violation.
273
+ association.reset
274
+ raise unless association.include?(@record)
275
+
276
+ association_already_exists
277
+ end
278
+
279
+ def association_already_exists
280
+ @association_already_exists = true
281
+ self.successful = true
282
+ flash[:warning] = as_(:already_added_model, model: ERB::Util.h(@record.to_label))
259
283
  end
260
284
 
261
285
  def do_destroy_existing
@@ -48,7 +48,7 @@ module ActiveScaffold::Actions
48
48
  # May be overridden to customize show routine
49
49
  def do_show
50
50
  set_includes_for_columns(:show) if active_scaffold_config.actions.include? :list
51
- get_row
51
+ get_record
52
52
  end
53
53
 
54
54
  # The default security delegates to ActiveRecordPermissions.
@@ -119,7 +119,7 @@ module ActiveScaffold
119
119
  key = "#{options[:crud_type]}##{options[:column]}##{options[:action]}"
120
120
  if is_a? Class
121
121
  self.class_security_methods ||= {}
122
- self.class_security_methods[key] ||= authorized_for_methods(options)
122
+ class_security_methods[key] ||= authorized_for_methods(options)
123
123
  else
124
124
  self.class.instance_security_methods ||= {}
125
125
  self.class.instance_security_methods[key] ||= authorized_for_methods(options)
@@ -267,7 +267,8 @@ module ActiveScaffold
267
267
  elsif klass.authorized_for?(crud_type: :create)
268
268
  association = parent_column.association
269
269
  record = klass.new
270
- if association.reverse_association&.belongs_to? && (association.collection? || current.nil?)
270
+ if association.reverse_association&.belongs_to? && association.singular? && current.nil?
271
+ # assign belongs_to on the new record, for has_one associations
271
272
  record.send(:"#{parent_column.association.reverse}=", parent_record)
272
273
  end
273
274
  record
@@ -32,6 +32,13 @@ module ActiveScaffold::Bridges
32
32
  end.join
33
33
  end
34
34
 
35
+ def self.date_options_by_locale
36
+ I18n.available_locales.filter_map do |locale|
37
+ locale_date_options = date_options(locale)
38
+ [locale.to_s, locale_date_options] if locale_date_options
39
+ end.to_h
40
+ end
41
+
35
42
  def self.date_options(locale)
36
43
  date_picker_options = {
37
44
  closeText: as_(:close),
@@ -76,6 +83,13 @@ module ActiveScaffold::Bridges
76
83
  end.join
77
84
  end
78
85
 
86
+ def self.datetime_options_by_locale
87
+ I18n.available_locales.filter_map do |locale|
88
+ locale_datetime_options = datetime_options(locale)
89
+ [locale.to_s, locale_datetime_options] if locale_datetime_options
90
+ end.to_h
91
+ end
92
+
79
93
  def self.datetime_options(locale)
80
94
  rails_time_format = I18n.translate! 'time.formats.picker', locale: locale, default: '%a, %d %b %Y %H:%M:%S'
81
95
  datetime_picker_options = {
@@ -30,5 +30,15 @@ module ActiveScaffold::Bridges
30
30
  }
31
31
  });\n"
32
32
  end
33
+
34
+ # Data-only equivalent of .localization, consumed directly by load.js without eval,
35
+ # so it works under a CSP that disallows 'unsafe-eval'.
36
+ def self.localization_data
37
+ {
38
+ locale: ::I18n.locale.to_s,
39
+ datepickerRegional: Helper.date_options_by_locale,
40
+ timepickerRegional: Helper.datetime_options_by_locale
41
+ }
42
+ end
33
43
  end
34
44
  end
@@ -33,6 +33,7 @@ module ActiveScaffold::Config
33
33
  @messages_above_header = self.class.messages_above_header
34
34
  @auto_select_columns = self.class.auto_select_columns
35
35
  @refresh_with_header = self.class.refresh_with_header
36
+ @local_sorting = self.class.local_sorting
36
37
  @calculate_etag = self.class.calculate_etag
37
38
  end
38
39
 
@@ -42,6 +43,10 @@ module ActiveScaffold::Config
42
43
  cattr_accessor :refresh_with_header, instance_accessor: false
43
44
  @@refresh_with_header = false
44
45
 
46
+ # enable sorting locally in the browser, when there is only one page
47
+ cattr_accessor :local_sorting, instance_accessor: false
48
+ @@local_sorting = false
49
+
45
50
  # how many records to show per page
46
51
  cattr_accessor :per_page, instance_accessor: false
47
52
  @@per_page = 15
@@ -134,6 +139,9 @@ module ActiveScaffold::Config
134
139
  # include list header on refresh
135
140
  attr_accessor :refresh_with_header
136
141
 
142
+ # enable sorting locally in the browser, when there is only one page
143
+ attr_accessor :local_sorting
144
+
137
145
  # how many rows to show at once
138
146
  attr_accessor :per_page
139
147
 
@@ -265,7 +273,8 @@ module ActiveScaffold::Config
265
273
  user_attr :page_links_inner_window, :page_links_outer_window, :refresh_with_header, :empty_field_text,
266
274
  :association_join_text, :messages_above_header, :wrap_tag, :auto_select_columns, :calculate_etag,
267
275
  :no_entries_message, :filtered_message, :show_search_reset, :always_show_create, :always_show_search,
268
- :hide_nested_column, :pagination, :auto_pagination, :filter_human_message, :show_filter_reset
276
+ :hide_nested_column, :pagination, :auto_pagination, :filter_human_message, :show_filter_reset,
277
+ :local_sorting
269
278
 
270
279
  def initialize(conf, storage, params)
271
280
  super(conf, storage, params, :list)
@@ -260,11 +260,11 @@ module ActiveScaffold
260
260
  raise ActiveScaffold::ControllerNotFound, "Could not find #{error_message.join(' or ')}", caller
261
261
  end
262
262
 
263
- def self.column_type_cast(value, column)
263
+ def self.column_type_cast(value, column, model)
264
264
  if defined?(ActiveRecord) && column.is_a?(ActiveRecord::ConnectionAdapters::Column)
265
- active_record_column_type_cast(value, column)
265
+ active_record_column_type_cast(value, column, model)
266
266
  elsif defined?(ActiveModel) && column.is_a?(ActiveModel::Attribute)
267
- active_record_column_type_cast(value, column.type)
267
+ active_record_column_type_cast(value, column.type, model)
268
268
  elsif defined?(Mongoid) && column.is_a?(Mongoid::Fields::Standard)
269
269
  mongoid_column_type_cast(value, column)
270
270
  else
@@ -278,10 +278,13 @@ module ActiveScaffold
278
278
  column.type.evolve value
279
279
  end
280
280
 
281
- def self.active_record_column_type_cast(value, column_or_type)
282
- return Time.zone.at(value.to_i) if %i[time datetime].include?(column_or_type.type) && value =~ /\A\d+\z/
281
+ def self.active_record_column_type_cast(value, column_or_type, model)
282
+ return Time.zone.at(value.to_i) if %i[time datetime timestamp timestamptz].include?(column_or_type.type) && value =~ /\A\d+\z/
283
283
 
284
- cast_type = column_or_type.is_a?(ActiveRecord::ConnectionAdapters::Column) ? ActiveRecord::Type.lookup(column_or_type.type) : column_or_type
284
+ cast_type = column_or_type
285
+ if cast_type.is_a?(ActiveRecord::ConnectionAdapters::Column)
286
+ cast_type = model.type_for_attribute(column_or_type.name)
287
+ end
285
288
  cast_type ? cast_type.cast(value) : value
286
289
  end
287
290
  end
@@ -64,6 +64,8 @@ module ActiveScaffold::DataStructures
64
64
  end
65
65
  @set = merged
66
66
  end
67
+ else
68
+ raise ArgumentError, "invalid layout mode: #{value.inspect}"
67
69
  end
68
70
  @layout = value
69
71
  end
@@ -41,6 +41,9 @@ module ActiveScaffold::DataStructures
41
41
  # a textual description of the column and its contents. this will be displayed with any associated form input widget, so you may want to consider adding a content example.
42
42
  attr_writer :description
43
43
 
44
+ # enable it if local sorting shouldn't be used for this column at all
45
+ attr_writer :skip_local_sorting
46
+
44
47
  attr_reader :update_columns
45
48
 
46
49
  # send all the form instead of only new value when this column changes
@@ -107,6 +110,9 @@ module ActiveScaffold::DataStructures
107
110
 
108
111
  # define the fields to use with logical search
109
112
  attr_accessor :logical_search
113
+
114
+ # SQL operator used for text searches. Defaults according to the adapter when nil.
115
+ attr_accessor :like_operator
110
116
  end
111
117
 
112
118
  def inplace_edit=(value)
@@ -125,6 +131,11 @@ module ActiveScaffold::DataStructures
125
131
  !(calculate == false || calculate.nil?)
126
132
  end
127
133
 
134
+ # if local sorting shouldn't be used for this column at all
135
+ def skip_local_sorting?
136
+ @skip_local_sorting == true
137
+ end
138
+
128
139
  def required?(action = nil)
129
140
  if action && @required
130
141
  @required == true || @required.include?(action)
@@ -345,7 +356,14 @@ module ActiveScaffold::DataStructures
345
356
 
346
357
  def search_sql
347
358
  initialize_search_sql if @search_sql == true
348
- @search_sql
359
+ return @search_sql unless @search_sql.is_a?(Array) && active_record?
360
+
361
+ connection = active_record_class.connection
362
+ @search_sql.map do |sql|
363
+ next sql unless sql.is_a?(Symbol)
364
+
365
+ "#{connection.quote_table_name(active_record_class.table_name)}.#{connection.quote_column_name(sql)}"
366
+ end
349
367
  end
350
368
 
351
369
  def searchable?
@@ -388,7 +406,7 @@ module ActiveScaffold::DataStructures
388
406
  def initialize_sort
389
407
  self.sort =
390
408
  if column && !tableless?
391
- {sql: field}
409
+ {sql: name}
392
410
  else
393
411
  false
394
412
  end
@@ -398,7 +416,7 @@ module ActiveScaffold::DataStructures
398
416
  self.search_sql =
399
417
  unless virtual?
400
418
  if association.nil?
401
- field.to_s unless tableless?
419
+ name unless tableless?
402
420
  elsif association.allow_join?
403
421
  [association.quoted_table_name, association.quoted_primary_key].join('.') unless association.klass < ActiveScaffold::Tableless
404
422
  end
@@ -517,14 +535,14 @@ module ActiveScaffold::DataStructures
517
535
  def initialize(name, active_record_class, delegated_association = nil) # :nodoc:
518
536
  @name = name.to_sym
519
537
  @active_record_class = active_record_class
520
- @column = _columns_hash[name.to_s]
538
+ @column = db_column = _columns_hash[name.to_s]
521
539
  if @column.nil? && active_record? && active_record_class._default_attributes.key?(name.to_s)
522
540
  @column = active_record_class._default_attributes[name.to_s]
523
541
  end
524
542
  @disable_on_update_column = true
525
543
  @db_default_value = ActiveScaffold::OrmChecks.default_value active_record_class, name if @column
526
544
  @delegated_association = delegated_association
527
- @cache_key = [@active_record_class.name, name].compact.map(&:to_s).join('#')
545
+ @cache_key = [@active_record_class.name, name].compact.join('#')
528
546
  setup_association_info
529
547
 
530
548
  @link = nil
@@ -538,6 +556,9 @@ module ActiveScaffold::DataStructures
538
556
  @select_columns = default_select_columns
539
557
 
540
558
  @text = @column.nil? || [:string, :text, :citext, String].include?(column_type)
559
+ if active_record? && db_column&.collation.present? && active_record_class.connection.adapter_name.in?(%w[PostgreSQL PostGIS])
560
+ @like_operator = 'LIKE'
561
+ end
541
562
  @number = false
542
563
  setup_defaults_for_column if @column
543
564
  @allow_add_existing = true
@@ -545,7 +566,7 @@ module ActiveScaffold::DataStructures
545
566
 
546
567
  self.includes = [association.name] if association&.allow_join?
547
568
  if delegated_association
548
- self.includes = includes ? [delegated_association.name => includes] : [delegated_association.name]
569
+ self.includes = includes ? [{delegated_association.name => includes}] : [delegated_association.name]
549
570
  end
550
571
  self.subform_includes = true if association
551
572