active_scaffold 3.5.5 → 3.6.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG → CHANGELOG.rdoc} +75 -0
  3. data/README.md +21 -10
  4. data/app/assets/javascripts/active_scaffold.js.erb +0 -1
  5. data/app/assets/javascripts/jquery/active_scaffold.js +98 -7
  6. data/app/assets/stylesheets/active_scaffold_colors.scss +1 -1
  7. data/app/assets/stylesheets/active_scaffold_layout.css +52 -29
  8. data/app/views/active_scaffold_overrides/_base_form.html.erb +2 -2
  9. data/app/views/active_scaffold_overrides/_form.html.erb +1 -1
  10. data/app/views/active_scaffold_overrides/_form_association.html.erb +2 -1
  11. data/app/views/active_scaffold_overrides/_form_association_footer.html.erb +3 -2
  12. data/app/views/active_scaffold_overrides/_form_association_record.html.erb +26 -10
  13. data/app/views/active_scaffold_overrides/_horizontal_subform.html.erb +4 -4
  14. data/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb +2 -1
  15. data/app/views/active_scaffold_overrides/_list.html.erb +2 -1
  16. data/app/views/active_scaffold_overrides/_list_header.html.erb +5 -7
  17. data/app/views/active_scaffold_overrides/_list_messages.html.erb +1 -0
  18. data/app/views/active_scaffold_overrides/_list_record.html.erb +4 -5
  19. data/app/views/active_scaffold_overrides/_list_with_header.html.erb +1 -1
  20. data/app/views/active_scaffold_overrides/_messages.html.erb +1 -0
  21. data/app/views/active_scaffold_overrides/_refresh_list.js.erb +4 -0
  22. data/app/views/active_scaffold_overrides/_render_field.js.erb +2 -1
  23. data/app/views/active_scaffold_overrides/_show_association_horizontal.html.erb +2 -1
  24. data/app/views/active_scaffold_overrides/_show_columns.html.erb +2 -2
  25. data/app/views/active_scaffold_overrides/_show_horizontal_record.html.erb +4 -4
  26. data/app/views/active_scaffold_overrides/_update_calculations.js.erb +1 -1
  27. data/app/views/active_scaffold_overrides/_update_column.js.erb +2 -2
  28. data/app/views/active_scaffold_overrides/_vertical_subform.html.erb +2 -2
  29. data/app/views/active_scaffold_overrides/action_confirmation.html.erb +2 -2
  30. data/app/views/active_scaffold_overrides/delete.html.erb +2 -2
  31. data/app/views/active_scaffold_overrides/on_action_update.js.erb +16 -6
  32. data/app/views/active_scaffold_overrides/on_update.js.erb +1 -1
  33. data/app/views/active_scaffold_overrides/row.js.erb +1 -1
  34. data/app/views/active_scaffold_overrides/update_column.js.erb +2 -2
  35. data/config/locales/de.yml +2 -1
  36. data/config/locales/en.yml +1 -0
  37. data/config/locales/es.yml +1 -0
  38. data/config/locales/fr.yml +2 -1
  39. data/config/locales/hu.yml +1 -0
  40. data/config/locales/ja.yml +1 -0
  41. data/config/locales/ru.yml +1 -0
  42. data/lib/active_scaffold.rb +19 -16
  43. data/lib/active_scaffold/actions/common_search.rb +11 -8
  44. data/lib/active_scaffold/actions/core.rb +91 -70
  45. data/lib/active_scaffold/actions/create.rb +28 -28
  46. data/lib/active_scaffold/actions/delete.rb +3 -3
  47. data/lib/active_scaffold/actions/field_search.rb +53 -43
  48. data/lib/active_scaffold/actions/list.rb +111 -27
  49. data/lib/active_scaffold/actions/nested.rb +65 -48
  50. data/lib/active_scaffold/actions/search.rb +1 -1
  51. data/lib/active_scaffold/actions/show.rb +4 -4
  52. data/lib/active_scaffold/actions/subform.rb +23 -22
  53. data/lib/active_scaffold/actions/update.rb +96 -77
  54. data/lib/active_scaffold/active_record_permissions.rb +2 -11
  55. data/lib/active_scaffold/attribute_params.rb +102 -94
  56. data/lib/active_scaffold/bridges.rb +8 -8
  57. data/lib/active_scaffold/bridges/active_storage.rb +6 -0
  58. data/lib/active_scaffold/bridges/active_storage/active_storage_bridge.rb +34 -0
  59. data/lib/active_scaffold/bridges/active_storage/active_storage_helpers.rb +54 -0
  60. data/lib/active_scaffold/bridges/active_storage/form_ui.rb +22 -0
  61. data/lib/active_scaffold/bridges/active_storage/list_ui.rb +36 -0
  62. data/lib/active_scaffold/bridges/bitfields.rb +2 -1
  63. data/lib/active_scaffold/bridges/bitfields/bitfields_bridge.rb +12 -15
  64. data/lib/active_scaffold/bridges/bitfields/list_ui.rb +19 -0
  65. data/lib/active_scaffold/bridges/calendar_date_select/as_cds_bridge.rb +1 -1
  66. data/lib/active_scaffold/bridges/cancan/cancan_bridge.rb +3 -12
  67. data/lib/active_scaffold/bridges/carrierwave/carrierwave_bridge.rb +1 -1
  68. data/lib/active_scaffold/bridges/carrierwave/list_ui.rb +2 -2
  69. data/lib/active_scaffold/bridges/chosen/helpers.rb +7 -6
  70. data/lib/active_scaffold/bridges/date_picker/ext.rb +0 -13
  71. data/lib/active_scaffold/bridges/date_picker/helper.rb +49 -44
  72. data/lib/active_scaffold/bridges/dragonfly/list_ui.rb +1 -1
  73. data/lib/active_scaffold/bridges/file_column/as_file_column_bridge.rb +1 -1
  74. data/lib/active_scaffold/bridges/file_column/file_column_helpers.rb +3 -3
  75. data/lib/active_scaffold/bridges/file_column/form_ui.rb +3 -3
  76. data/lib/active_scaffold/bridges/file_column/test/functional/file_column_keep_test.rb +10 -7
  77. data/lib/active_scaffold/bridges/paper_trail.rb +1 -1
  78. data/lib/active_scaffold/bridges/paper_trail/actions.rb +3 -1
  79. data/lib/active_scaffold/bridges/paperclip/list_ui.rb +1 -1
  80. data/lib/active_scaffold/bridges/paperclip/paperclip_bridge.rb +1 -1
  81. data/lib/active_scaffold/bridges/paperclip/paperclip_bridge_helpers.rb +2 -2
  82. data/lib/active_scaffold/bridges/record_select/helpers.rb +15 -17
  83. data/lib/active_scaffold/bridges/shared/date_bridge.rb +20 -19
  84. data/lib/active_scaffold/bridges/tiny_mce/helpers.rb +3 -1
  85. data/lib/active_scaffold/bridges/usa_state_select/usa_state_select_helper.rb +21 -4
  86. data/lib/active_scaffold/config/base.rb +133 -41
  87. data/lib/active_scaffold/config/core.rb +146 -18
  88. data/lib/active_scaffold/config/delete.rb +14 -1
  89. data/lib/active_scaffold/config/field_search.rb +7 -1
  90. data/lib/active_scaffold/config/form.rb +10 -1
  91. data/lib/active_scaffold/config/list.rb +39 -13
  92. data/lib/active_scaffold/config/mark.rb +4 -2
  93. data/lib/active_scaffold/config/nested.rb +16 -17
  94. data/lib/active_scaffold/config/search.rb +9 -0
  95. data/lib/active_scaffold/config/show.rb +4 -0
  96. data/lib/active_scaffold/config/update.rb +4 -0
  97. data/lib/active_scaffold/configurable.rb +14 -7
  98. data/lib/active_scaffold/constraints.rb +22 -20
  99. data/lib/active_scaffold/core.rb +67 -28
  100. data/lib/active_scaffold/data_structures/action_columns.rb +50 -59
  101. data/lib/active_scaffold/data_structures/action_link.rb +50 -20
  102. data/lib/active_scaffold/data_structures/action_links.rb +15 -13
  103. data/lib/active_scaffold/data_structures/association/abstract.rb +38 -15
  104. data/lib/active_scaffold/data_structures/association/active_mongoid.rb +2 -6
  105. data/lib/active_scaffold/data_structures/association/active_record.rb +6 -2
  106. data/lib/active_scaffold/data_structures/association/mongoid.rb +0 -3
  107. data/lib/active_scaffold/data_structures/column.rb +75 -66
  108. data/lib/active_scaffold/data_structures/columns.rb +3 -2
  109. data/lib/active_scaffold/data_structures/nested_info.rb +33 -19
  110. data/lib/active_scaffold/data_structures/set.rb +8 -0
  111. data/lib/active_scaffold/data_structures/sorting.rb +10 -2
  112. data/lib/active_scaffold/delayed_setup.rb +16 -5
  113. data/lib/active_scaffold/extensions/action_controller_rendering.rb +3 -2
  114. data/lib/active_scaffold/extensions/action_view_rendering.rb +93 -32
  115. data/lib/active_scaffold/extensions/cow_proxy.rb +95 -0
  116. data/lib/active_scaffold/extensions/ice_nine.rb +36 -0
  117. data/lib/active_scaffold/extensions/left_outer_joins.rb +8 -33
  118. data/lib/active_scaffold/extensions/localize.rb +3 -1
  119. data/lib/active_scaffold/extensions/routing_mapper.rb +6 -45
  120. data/lib/active_scaffold/extensions/to_label.rb +3 -2
  121. data/lib/active_scaffold/extensions/unsaved_record.rb +2 -4
  122. data/lib/active_scaffold/finder.rb +110 -77
  123. data/lib/active_scaffold/helpers/action_link_helpers.rb +62 -36
  124. data/lib/active_scaffold/helpers/association_helpers.rb +18 -16
  125. data/lib/active_scaffold/helpers/controller_helpers.rb +34 -10
  126. data/lib/active_scaffold/helpers/form_column_helpers.rb +196 -124
  127. data/lib/active_scaffold/helpers/human_condition_helpers.rb +1 -1
  128. data/lib/active_scaffold/helpers/id_helpers.rb +6 -2
  129. data/lib/active_scaffold/helpers/list_column_helpers.rb +90 -57
  130. data/lib/active_scaffold/helpers/pagination_helpers.rb +2 -2
  131. data/lib/active_scaffold/helpers/search_column_helpers.rb +43 -41
  132. data/lib/active_scaffold/helpers/show_column_helpers.rb +3 -5
  133. data/lib/active_scaffold/helpers/view_helpers.rb +39 -36
  134. data/lib/active_scaffold/marked_model.rb +2 -2
  135. data/lib/active_scaffold/orm_checks.rb +3 -7
  136. data/lib/active_scaffold/paginator.rb +7 -7
  137. data/lib/active_scaffold/registry.rb +33 -0
  138. data/lib/active_scaffold/responds_to_parent.rb +8 -11
  139. data/lib/active_scaffold/tableless.rb +83 -67
  140. data/lib/active_scaffold/version.rb +2 -2
  141. data/lib/generators/active_scaffold/controller_generator.rb +2 -2
  142. data/lib/generators/active_scaffold/install_generator.rb +52 -4
  143. data/lib/generators/active_scaffold/resource_generator.rb +2 -2
  144. data/shoulda_macros/macros.rb +3 -1
  145. data/test/bridges/date_picker_test.rb +1 -2
  146. data/test/bridges/paperclip_test.rb +6 -6
  147. data/test/class_with_finder.rb +2 -2
  148. data/test/company.rb +4 -4
  149. data/test/config/create_test.rb +4 -2
  150. data/test/config/nested_test.rb +1 -1
  151. data/test/config/show_test.rb +1 -1
  152. data/test/config/update_test.rb +7 -6
  153. data/test/data_structures/action_columns_test.rb +2 -2
  154. data/test/data_structures/action_links_test.rb +1 -1
  155. data/test/data_structures/column_test.rb +3 -6
  156. data/test/data_structures/columns_test.rb +2 -2
  157. data/test/data_structures/sorting_test.rb +7 -0
  158. data/test/extensions/action_view_rendering_test.rb +20 -0
  159. data/test/extensions/active_record_test.rb +4 -4
  160. data/test/extensions/routing_mapper_test.rb +2 -2
  161. data/test/helpers/list_column_helpers_test.rb +3 -1
  162. data/test/misc/active_record_permissions_test.rb +3 -11
  163. data/test/misc/attribute_params_test.rb +12 -8
  164. data/test/misc/calculation_test.rb +1 -1
  165. data/test/misc/configurable_test.rb +10 -10
  166. data/test/misc/constraints_test.rb +3 -3
  167. data/test/misc/convert_numbers_format_test.rb +7 -3
  168. data/test/misc/lang_test.rb +1 -1
  169. data/test/misc/parse_datetime_test.rb +3 -4
  170. data/test/misc/tableless_test.rb +14 -0
  171. data/test/mock_app/Rakefile +1 -1
  172. data/test/mock_app/app/assets/config/manifest.js +0 -0
  173. data/test/mock_app/app/controllers/cars_controller.rb +1 -0
  174. data/test/mock_app/app/controllers/people_controller.rb +5 -1
  175. data/test/mock_app/app/controllers/roles_controller.rb +4 -0
  176. data/test/mock_app/app/views/active_scaffold_overrides/_form.html.erb +2 -0
  177. data/test/mock_app/app/views/active_scaffold_overrides/list.html.erb +2 -0
  178. data/test/mock_app/app/views/people/_first_name_form_column.html.erb +2 -0
  179. data/test/mock_app/app/views/people/_form.html.erb +2 -0
  180. data/test/mock_app/app/views/people/list.html.erb +2 -0
  181. data/test/mock_app/config/application.rb +2 -1
  182. data/test/mock_app/config/boot.rb +1 -1
  183. data/test/mock_app/config/environment.rb +2 -2
  184. data/test/mock_app/config/routes.rb +4 -1
  185. data/test/mock_app/db/schema.rb +2 -0
  186. data/test/performance/list_cars_performance_test.rb +34 -0
  187. data/test/performance/list_people_performance_test.rb +31 -0
  188. data/test/performance_test_help.rb +3 -0
  189. data/test/test_helper.rb +12 -4
  190. metadata +71 -15
  191. data/app/assets/javascripts/prototype/rico_corner.js +0 -370
  192. data/lib/active_scaffold/bridges/file_column/test/test_helper.rb +0 -7
@@ -24,9 +24,9 @@ module ActiveScaffold
24
24
  ul_options = record_or_ul_options if ul_options.nil? && record_or_ul_options.is_a?(Hash)
25
25
  record = record_or_ul_options unless record_or_ul_options.is_a?(Hash)
26
26
  html = content_tag :ul, ul_options do
27
- safe_join links.map { |link| content_tag :li, link }
27
+ safe_join(links.map { |link| content_tag :li, link })
28
28
  end
29
- raw "ActiveScaffold.display_dynamic_action_group('#{get_action_link_id action_link, record}', '#{escape_javascript html}');"
29
+ raw "ActiveScaffold.display_dynamic_action_group('#{get_action_link_id action_link, record}', '#{escape_javascript html}');" # rubocop:disable Rails/OutputSafety
30
30
  end
31
31
 
32
32
  def display_action_links(action_links, record, options, &block)
@@ -67,7 +67,7 @@ module ActiveScaffold
67
67
  html_classes << 'top' if options[:first_action]
68
68
  group_tag = :li
69
69
  end
70
- content = content_tag(group_tag, :class => (html_classes if html_classes.present?), :onclick => ('' if hover_via_click?)) do
70
+ content = content_tag(group_tag, :class => html_classes.presence, :onclick => ('' if hover_via_click?)) do
71
71
  content_tag(:div, as_(link.label), :class => link.name.to_s.downcase) << content_tag(:ul, content)
72
72
  end
73
73
  else
@@ -79,13 +79,15 @@ module ActiveScaffold
79
79
  end
80
80
 
81
81
  def render_action_link(link, record = nil, options = {})
82
- if link.action.nil? || link.column.try(:association).try(:polymorphic?)
83
- link = action_link_to_inline_form(link, record) if link.column.try(:association)
82
+ if link.action.nil? || link.column&.association&.polymorphic?
83
+ link = action_link_to_inline_form(link, record) if link.column&.association
84
84
  options[:authorized] = false if link.action.nil? || link.controller.nil?
85
85
  options.delete :link if link.crud_type == :create
86
86
  end
87
87
  if link.action.nil? || (link.type == :member && options.key?(:authorized) && !options[:authorized])
88
- action_link_html(link, nil, {:link => action_link_text(link, options), :class => "disabled #{link.action}#{" #{link.html_options[:class]}" if link.html_options[:class].present?}", :title => options[:not_authorized_reason]}, record)
88
+ html_class = "disabled #{link.action}#{" #{link.html_options[:class]}" if link.html_options[:class].present?}"
89
+ html_options = {:link => action_link_text(link, options), :class => html_class, :title => options[:not_authorized_reason]}
90
+ action_link_html(link, nil, html_options, record)
89
91
  else
90
92
  url = action_link_url(link, record)
91
93
  html_options = action_link_html_options(link, record, options)
@@ -95,9 +97,9 @@ module ActiveScaffold
95
97
 
96
98
  # setup the action link to inline form
97
99
  def action_link_to_inline_form(link, record)
98
- link = link.clone
100
+ link = link.dup
99
101
  associated = record.send(link.column.association.name)
100
- if link.column.association.try(:polymorphic?)
102
+ if link.column.association&.polymorphic?
101
103
  link.controller = controller_path_for_activerecord(associated.class)
102
104
  return link if link.controller.nil?
103
105
  end
@@ -106,12 +108,12 @@ module ActiveScaffold
106
108
  end
107
109
 
108
110
  def configure_column_link(link, record, associated, actions = nil)
109
- actions ||= link.column.actions_for_association_links
111
+ actions ||= link.controller_actions || []
110
112
  if column_empty?(associated) # if association is empty, we only can link to create form
111
113
  if actions.include?(:new)
112
114
  link.action = 'new'
113
115
  link.crud_type = :create
114
- link.label ||= as_(:create_new)
116
+ link.label ||= :create_new
115
117
  end
116
118
  elsif actions.include?(:edit)
117
119
  link.action = 'edit'
@@ -137,7 +139,7 @@ module ActiveScaffold
137
139
  def column_link_authorized?(link, column, record, associated)
138
140
  if column.association
139
141
  associated_for_authorized =
140
- if column.association.collection? || (associated.respond_to?(:blank?) && associated.blank?)
142
+ if column.association.collection? || associated.nil?
141
143
  column.association.klass
142
144
  else
143
145
  associated
@@ -160,7 +162,7 @@ module ActiveScaffold
160
162
  end
161
163
 
162
164
  def cache_action_link_url?(link, record)
163
- active_scaffold_config.cache_action_link_urls && link.type == :member && !link.dynamic_parameters.is_a?(Proc) && !sti_record?(record)
165
+ active_scaffold_config.user.cache_action_link_urls && link.type == :member && !link.dynamic_parameters.is_a?(Proc) && !sti_record?(record)
164
166
  end
165
167
 
166
168
  def cached_action_link_url(link, record)
@@ -178,8 +180,8 @@ module ActiveScaffold
178
180
 
179
181
  def replace_id_params_in_action_link_url(link, record, url)
180
182
  url = record ? url.sub('--ID--', record.to_param.to_s) : url.clone
181
- if link.column.try(:association).try(:singular?)
182
- child_id = record.send(link.column.association.name).try(:to_param)
183
+ if link.column&.association&.singular?
184
+ child_id = record.send(link.column.association.name)&.to_param
183
185
  if child_id.present?
184
186
  url.sub!('--CHILD_ID--', child_id)
185
187
  else
@@ -209,6 +211,18 @@ module ActiveScaffold
209
211
  url
210
212
  end
211
213
 
214
+ def column_in_params_conditions?(key)
215
+ if key =~ /!$/
216
+ conditions_from_params[1..-1].any? { |node| node.left.name.to_s == key[0..-2] }
217
+ else
218
+ conditions_from_params[0].include?(key)
219
+ end
220
+ end
221
+
222
+ def ignore_param_for_nested?(key)
223
+ NESTED_PARAMS.include?(key) || column_in_params_conditions?(key) || (nested? && nested.param_name == key)
224
+ end
225
+
212
226
  def query_string_for_action_links(link)
213
227
  if defined?(@query_string) && link.parameters.none? { |k, _| @query_string_params.include? k }
214
228
  return [@query_string, @non_nested_query_string]
@@ -224,12 +238,17 @@ module ActiveScaffold
224
238
  keep = false
225
239
  next
226
240
  end
227
- if NESTED_PARAMS.include?(key) || conditions_from_params.include?(key) || (nested? && nested.param_name == key)
241
+ if ignore_param_for_nested?(key)
228
242
  non_nested_query_string_options[key] = value
229
243
  else
230
244
  query_string_options[key] = value
231
245
  end
232
246
  end
247
+ if nested_singular_association? && action_name == 'index'
248
+ # pass current path as return_to, for nested listing on singular association, so forms doesn't return to parent listing
249
+ @query_string_params << :return_to
250
+ non_nested_query_string_options[:return_to] = request.fullpath
251
+ end
233
252
 
234
253
  query_string = query_string_options.to_query if query_string_options.present?
235
254
  if non_nested_query_string_options.present?
@@ -243,7 +262,7 @@ module ActiveScaffold
243
262
  end
244
263
 
245
264
  def cache_action_link_url_options?(link, record)
246
- active_scaffold_config.cache_action_link_urls && (link.type == :collection || !link.dynamic_parameters.is_a?(Proc)) && !sti_record?(record)
265
+ active_scaffold_config.user.cache_action_link_urls && (link.type == :collection || !link.dynamic_parameters.is_a?(Proc)) && !sti_record?(record)
247
266
  end
248
267
 
249
268
  def cached_action_link_url_options(link, record)
@@ -290,8 +309,8 @@ module ActiveScaffold
290
309
  missing_options, url_options = url.partition { |_, v| v.nil? }
291
310
  replacements = {}
292
311
  replacements['--ID--'] = record.id.to_s if record
293
- if link.column.try(:association).try(:singular?)
294
- replacements['--CHILD_ID--'] = record.send(link.column.association.name).try(:id).to_s
312
+ if link.column&.association&.singular?
313
+ replacements['--CHILD_ID--'] = record.send(link.column.association.name)&.id.to_s
295
314
  elsif nested?
296
315
  replacements['--CHILD_ID--'] = params[nested.param_name].to_s
297
316
  end
@@ -303,7 +322,7 @@ module ActiveScaffold
303
322
 
304
323
  def action_link_selected?(link, record)
305
324
  missing_options, url_options = replaced_action_link_url_options(link, record)
306
- safe_params = (Rails.version < '4.2' ? params.to_h : params.to_unsafe_h)
325
+ safe_params = params.to_unsafe_h
307
326
  (url_options - safe_params.to_a).blank? && missing_options.all? { |k, _| params[k].nil? }
308
327
  end
309
328
 
@@ -316,7 +335,7 @@ module ActiveScaffold
316
335
  html_options[:method] = link.method if link.method != :get
317
336
 
318
337
  html_options[:data] ||= {}
319
- html_options[:data][:confirm] = link.confirm(h(record.try(:to_label))) if link.confirm?
338
+ html_options[:data][:confirm] = link.confirm(h(record&.to_label)) if link.confirm?
320
339
  if !options[:page] && !options[:popup] && (options[:inline] || link.inline?)
321
340
  html_options[:class] << ' as_action'
322
341
  html_options[:data][:position] = link.position if link.position
@@ -331,7 +350,10 @@ module ActiveScaffold
331
350
  html_options[:class] << ' active' if action_link_selected?(link, record)
332
351
  end
333
352
 
334
- html_options[:target] = '_blank' if !options[:page] && !options[:inline] && (options[:popup] || link.popup?)
353
+ if !options[:page] && !options[:inline] && (options[:popup] || link.popup?)
354
+ html_options[:target] = '_blank'
355
+ html_options[:rel] = [html_options[:rel], 'noopener noreferrer'].compact.join(' ')
356
+ end
335
357
  html_options[:id] = link_id
336
358
  if link.dhtml_confirm?
337
359
  unless link.inline?
@@ -344,20 +366,23 @@ module ActiveScaffold
344
366
  html_options
345
367
  end
346
368
 
347
- def get_action_link_id(link, record = nil, column = nil)
348
- column ||= link.column
349
- if column.try(:association) && record
350
- id = if column.association.collection?
351
- "#{column.association.name}-#{record.id}"
352
- elsif record.send(column.association.name).present?
353
- "#{column.association.name}-#{record.send(column.association.name).id}-#{record.id}"
354
- else
355
- "#{column.association.name}-#{record.id}"
356
- end
369
+ def get_action_link_id(link, record = nil)
370
+ column = link.column
371
+ if column&.association && record
372
+ associated = record.send(column.association.name) unless column.association.collection?
373
+ id =
374
+ if associated
375
+ "#{column.association.name}-#{associated.id}-#{record.id}"
376
+ else
377
+ "#{column.association.name}-#{record.id}"
378
+ end
379
+ end
380
+ id ||= record&.id&.to_s || (nested? ? nested_parent_id.to_s : '')
381
+ action_link_id = ActiveScaffold::Registry.cache :action_link_id, link.name_to_cache do
382
+ action_id = "#{id_from_controller("#{link.controller}-") if params[:parent_controller] || (link.controller && link.controller != controller.controller_path)}#{link.action}"
383
+ action_link_id(action_id, '--ID--')
357
384
  end
358
- id ||= record.try(:id) || (nested? ? nested_parent_id : '')
359
- action_id = "#{id_from_controller("#{link.controller}-") if params[:parent_controller] || (link.controller && link.controller != controller.controller_path)}#{link.action}"
360
- action_link_id(action_id, id)
385
+ action_link_id.sub('--ID--', id)
361
386
  end
362
387
 
363
388
  def action_link_html(link, url, html_options, record)
@@ -371,15 +396,16 @@ module ActiveScaffold
371
396
  end
372
397
 
373
398
  def url_options_for_nested_link(column, record, link, url_options)
374
- if column.try(:association)
399
+ if column&.association
375
400
  url_options[:parent_scaffold] = controller_path
376
401
  url_options[column.model.name.foreign_key.to_sym] = url_options.delete(:id)
377
402
  url_options[:id] = if column.association.singular? && url_options[:action].to_sym != :index
378
403
  '--CHILD_ID--'
379
404
  end
380
- elsif link.parameters && link.parameters[:named_scope]
405
+ elsif link.parameters&.dig(:named_scope)
381
406
  url_options[:parent_scaffold] = controller_path
382
407
  url_options[active_scaffold_config.model.name.foreign_key.to_sym] = url_options.delete(:id)
408
+ url_options[:id] = nil
383
409
  end
384
410
  end
385
411
 
@@ -16,11 +16,8 @@ module ActiveScaffold
16
16
  def association_options_find(association, conditions = nil, klass = nil, record = nil)
17
17
  if klass.nil? && association.polymorphic?
18
18
  class_name = record.send(association.foreign_type) if association.belongs_to?
19
- if class_name.present?
20
- klass = class_name.constantize
21
- else
22
- return []
23
- end
19
+ return [] if class_name.blank?
20
+ klass = class_name.constantize
24
21
  cache = !block_given?
25
22
  else
26
23
  cache = !block_given? && klass.nil?
@@ -32,12 +29,14 @@ module ActiveScaffold
32
29
  klass = association_klass_scoped(association, klass, record)
33
30
  relation = klass.where(conditions)
34
31
  column = column_for_association(association, record)
35
- if column && column.includes
32
+ if column&.includes
36
33
  include_assoc = column.includes.find { |assoc| assoc.is_a?(Hash) && assoc.include?(association.name) }
37
34
  relation = relation.includes(include_assoc[association.name]) if include_assoc
38
35
  end
39
- if column && column.sort && column.sort[:sql]
40
- relation = relation.order(column.sort[:sql])
36
+ if column&.sort && column.sort&.dig(:sql)
37
+ # with threasafe enabled, column.sort[:sql] returns proxied strings and
38
+ # regexp capture won't work, which rails uses internally, so to_s is needed
39
+ relation = relation.order(Array(column.sort[:sql]).map(&:to_s))
41
40
  end
42
41
  relation = yield(relation) if block_given?
43
42
  relation.to_a
@@ -45,16 +44,20 @@ module ActiveScaffold
45
44
  end
46
45
 
47
46
  def column_for_association(association, record)
48
- active_scaffold_config_for(record.class).columns[association.name] rescue nil
47
+ active_scaffold_config_for(record.class).columns[association.name]
48
+ rescue StandardError => e
49
+ message = "Error on config for #{record.class.name}:"
50
+ Rails.logger.warn "#{message}\n#{e.message}\n#{e.backtrace.join("\n")}"
51
+ nil
49
52
  end
50
53
 
51
54
  def association_klass_scoped(association, klass, record)
52
- if nested? && nested.through_association? && nested.child_association.try(:through_reflection) == association
55
+ if nested? && nested.through_association? && nested.child_association&.through_reflection == association
53
56
  # only ActiveRecord associations
54
57
  if nested.association.through_reflection.collection?
55
58
  nested_parent_record.send(nested.association.through_reflection.name)
56
59
  else
57
- klass.where(association.association_primary_key => nested_parent_record.send(nested.association.through_reflection.name).try(:id))
60
+ klass.where(association.association_primary_key => nested_parent_record.send(nested.association.through_reflection.name)&.id)
58
61
  end
59
62
  else
60
63
  klass
@@ -65,7 +68,7 @@ module ActiveScaffold
65
68
  def sorted_association_options_find(association, conditions = nil, record = nil)
66
69
  options = association_options_find(association, conditions, nil, record)
67
70
  column = column_for_association(association, record)
68
- unless column && column.try(:sort) && column.sort[:sql]
71
+ unless column&.sort && column.sort&.dig(:sql)
69
72
  method = column.options[:label_method] if column
70
73
  options = options.sort_by(&(method || :to_label).to_sym)
71
74
  end
@@ -86,10 +89,9 @@ module ActiveScaffold
86
89
  # Check association.name to specialize the conditions per-column.
87
90
  def options_for_association_conditions(association, record = nil)
88
91
  return nil if association.through?
89
- if association.has_one? || association.has_many?
90
- # Find only orphaned objects
91
- {association.foreign_key => nil}
92
- end
92
+ return nil unless association.has_one? || association.has_many?
93
+ # Find only orphaned objects
94
+ {association.foreign_key => nil}
93
95
  end
94
96
 
95
97
  def record_select_params_for_add_existing(association, edit_associated_url_options, record)
@@ -2,25 +2,38 @@ module ActiveScaffold
2
2
  module Helpers
3
3
  module ControllerHelpers
4
4
  def self.included(controller)
5
- if controller.respond_to? :helper_method
6
- controller.class_eval do
7
- helper_method :params_for, :conditions_from_params, :render_parent?,
8
- :main_path_to_return, :render_parent_options,
9
- :render_parent_action, :nested_singular_association?,
10
- :main_form_controller, :build_associated,
11
- :generate_temporary_id, :generated_id
12
- end
5
+ return unless controller.respond_to? :helper_method
6
+ controller.class_eval do
7
+ helper_method :params_for, :conditions_from_params, :render_parent?,
8
+ :main_path_to_return, :render_parent_options,
9
+ :render_parent_action, :nested_singular_association?,
10
+ :main_form_controller, :build_associated,
11
+ :generate_temporary_id, :generated_id,
12
+ :active_scaffold_config_for
13
13
  end
14
14
  end
15
15
 
16
16
  include ActiveScaffold::Helpers::IdHelpers
17
17
 
18
+ def active_scaffold_config_for(klass)
19
+ config = self.class.active_scaffold_config_for(klass)
20
+ if ActiveScaffold.threadsafe
21
+ config.user || config.new_user_settings({}, {})
22
+ else
23
+ config
24
+ end
25
+ end
26
+
18
27
  def generate_temporary_id(record = nil, generated_id = nil)
19
28
  (generated_id || (Time.now.to_f * 1000).to_i.to_s).tap do |id|
20
- (@temporary_ids ||= {})[record.class.name] = id if record
29
+ cache_generated_id record, id
21
30
  end
22
31
  end
23
32
 
33
+ def cache_generated_id(record, generated_id)
34
+ (@temporary_ids ||= {})[record.class.name] = generated_id if record && generated_id
35
+ end
36
+
24
37
  def generated_id(record)
25
38
  @temporary_ids[record.class.name] if record && @temporary_ids
26
39
  end
@@ -91,7 +104,7 @@ module ActiveScaffold
91
104
  else
92
105
  exclude_parameters = %i[utf8 associated_id]
93
106
  parameters = {}
94
- if params[:parent_scaffold] && nested? && nested.singular_association?
107
+ if params[:parent_scaffold] && nested_singular_association?
95
108
  parameters[:controller] = params[:parent_scaffold]
96
109
  exclude_parameters.concat [nested.param_name, :association, :parent_scaffold]
97
110
  # parameters[:eid] = params[:parent_scaffold] # not neeeded anymore?
@@ -167,6 +180,17 @@ module ActiveScaffold
167
180
  end
168
181
  end
169
182
  end
183
+
184
+ def save_record_to_association(record, association, value, reverse = nil)
185
+ return unless association
186
+ if association.collection?
187
+ record.association(association.name).target << value
188
+ elsif reverse&.belongs_to?
189
+ value.send("#{reverse.name}=", record)
190
+ else
191
+ record.send("#{association.name}=", value)
192
+ end
193
+ end
170
194
  end
171
195
  end
172
196
  end
@@ -16,47 +16,43 @@ module ActiveScaffold
16
16
  # first, check if the dev has created an override for this specific field
17
17
  if (method = override_form_field(column))
18
18
  send(method, record, options)
19
+
19
20
  # second, check if the dev has specified a valid form_ui for this column
20
21
  elsif column.form_ui && (method = override_input(column.form_ui))
21
22
  send(method, column, options)
22
- # fallback: we get to make the decision
23
- else
24
- if column.association
25
- if column.form_ui.nil?
26
- # its an association and nothing is specified, we will assume form_ui :select
27
- active_scaffold_input_select(column, options)
28
- else
29
- # if we get here, it's because the column has a form_ui but not one ActiveScaffold knows about.
30
- raise "Unknown form_ui `#{column.form_ui}' for column `#{column.name}'"
31
- end
32
- elsif column.virtual?
33
- options[:value] = format_number_value(record.send(column.name), column.options) if column.number?
34
- active_scaffold_input_virtual(column, options)
35
-
36
- else # regular model attribute column
37
- # if we (or someone else) have created a custom render option for the column type, use that
38
- if (method = override_input(column.column.type))
39
- send(method, column, options)
40
- # final ultimate fallback: use rails' generic input method
41
- else
42
- # for textual fields we pass different options
43
- text_types = %i[text string integer float decimal]
44
- options = active_scaffold_input_text_options(options) if text_types.include?(column.column.type)
45
- if column.column.type == :string && options[:maxlength].blank?
46
- options[:maxlength] = column.column.limit
47
- options[:size] ||= options[:maxlength].to_i > 30 ? 30 : options[:maxlength]
48
- end
49
- options[:value] = format_number_value(record.send(column.name), column.options) if column.number?
50
- text_field(:record, column.name, options.merge(column.options).except(:format))
51
- end
23
+
24
+ elsif column.association
25
+ # if we get here, it's because the column has a form_ui but not one ActiveScaffold knows about.
26
+ raise "Unknown form_ui `#{column.form_ui}' for column `#{column.name}'" if column.form_ui
27
+
28
+ # its an association and nothing is specified, we will assume form_ui :select
29
+ active_scaffold_input_select(column, options)
30
+
31
+ elsif column.virtual?
32
+ options[:value] = format_number_value(record.send(column.name), column.options) if column.number?
33
+ active_scaffold_input_virtual(column, options)
34
+
35
+ elsif (method = override_input(column.column.type)) # regular model attribute column
36
+ # if we (or someone else) have created a custom render option for the column type, use that
37
+ send(method, column, options)
38
+
39
+ else # final ultimate fallback: use rails' generic input method
40
+ # for textual fields we pass different options
41
+ text_types = %i[text string integer float decimal]
42
+ options = active_scaffold_input_text_options(options) if text_types.include?(column.column.type)
43
+ if column.column.type == :string && options[:maxlength].blank?
44
+ options[:maxlength] = column.column.limit
45
+ options[:size] ||= options[:maxlength].to_i > 30 ? 30 : options[:maxlength]
52
46
  end
47
+ options[:value] = format_number_value(record.send(column.name), column.options) if column.number?
48
+ text_field(:record, column.name, options.merge(column.options).except(:format))
53
49
  end
54
50
  rescue StandardError => e
55
51
  logger.error "#{e.class.name}: #{e.message} -- on the ActiveScaffold column = :#{column.name} in #{controller.class}"
56
52
  raise e
57
53
  end
58
54
 
59
- def active_scaffold_render_subform_column(column, scope, crud_type, readonly, add_class = false, record = nil)
55
+ def active_scaffold_render_subform_column(column, scope, crud_type, readonly, add_class = false, record = nil) # rubocop:disable Metrics/ParameterLists
60
56
  if add_class
61
57
  col_class = []
62
58
  col_class << 'required' if column.required?
@@ -66,9 +62,7 @@ module ActiveScaffold
66
62
  col_class = col_class.join(' ')
67
63
  end
68
64
  if readonly && !record.new_record? || !record.authorized_for?(:crud_type => crud_type, :column => column.name)
69
- options = active_scaffold_input_options(column, scope).except(:name)
70
- options[:class] = "#{options[:class]} #{col_class}" if col_class
71
- content_tag :span, get_column_value(record, column), options
65
+ form_attribute(column, record, scope, true, col_class)
72
66
  else
73
67
  renders_as = column_renders_as(column)
74
68
  html = render_column(column, record, renders_as, scope, false, col_class)
@@ -77,16 +71,16 @@ module ActiveScaffold
77
71
  end
78
72
  end
79
73
 
80
- def active_scaffold_subform_attributes(column, column_css_class = nil)
74
+ def active_scaffold_subform_attributes(column, column_css_class = nil, klass = nil)
81
75
  {
82
- :class => "sub-form #{active_scaffold_config_for(column.association.klass).subform.layout}-sub-form #{column_css_class} #{column.name}-sub-form",
76
+ :class => "sub-form #{active_scaffold_config_for(klass || column.association.klass).subform.layout}-sub-form #{column_css_class} #{column.name}-sub-form",
83
77
  :id => sub_form_id(:association => column.name)
84
78
  }
85
79
  end
86
80
 
87
81
  # the standard active scaffold options used for textual inputs
88
82
  def active_scaffold_input_text_options(options = {})
89
- options[:autocomplete] = 'off'
83
+ options[:autocomplete] ||= 'off'
90
84
  options[:class] = "#{options[:class]} text-input".strip
91
85
  options
92
86
  end
@@ -97,7 +91,7 @@ module ActiveScaffold
97
91
  record = options[:object]
98
92
 
99
93
  # Add some HTML5 attributes for in-browser validation and better user experience
100
- if column.required? && (!@disable_required_for_new || scope.nil? || record.try(:persisted?))
94
+ if column.required? && (!@disable_required_for_new || scope.nil? || record&.persisted?)
101
95
  options[:required] = true
102
96
  end
103
97
  options[:placeholder] = column.placeholder if column.placeholder.present?
@@ -114,18 +108,20 @@ module ActiveScaffold
114
108
 
115
109
  def current_form_columns(record, scope, subform_controller = nil)
116
110
  if scope
117
- subform_controller.active_scaffold_config.subform.columns.names
111
+ subform_controller.active_scaffold_config.subform.columns.visible_columns_names
118
112
  elsif %i[new create edit update render_field].include? action_name.to_sym
119
113
  # disable update_columns for inplace_edit (GET render_field)
120
114
  return if action_name == 'render_field' && request.get?
121
- active_scaffold_config.send(record.new_record? ? :create : :update).columns.names
115
+ active_scaffold_config.send(record.new_record? ? :create : :update).columns.visible_columns_names
122
116
  end
123
117
  end
124
118
 
125
119
  def update_columns_options(column, scope, options, force = false)
126
120
  record = options[:object]
127
121
  subform_controller = controller.class.active_scaffold_controller_for(record.class) if scope
128
- form_columns = @main_columns.try(:names) if scope.nil? || subform_controller == controller.class
122
+ if @main_columns && (scope.nil? || subform_controller == controller.class)
123
+ form_columns = @main_columns.visible_columns_names
124
+ end
129
125
  form_columns ||= current_form_columns(record, scope, subform_controller)
130
126
  if force || (form_columns && column.update_columns && (column.update_columns & form_columns).present?)
131
127
  url_params = params_for(:action => 'render_field', :column => column.name, :id => record.to_param)
@@ -134,7 +130,7 @@ module ActiveScaffold
134
130
  url_params = url_params.except(:parent_scaffold, :association, nested.param_name)
135
131
  end
136
132
  if scope
137
- url_params[:parent_controller] ||= url_params[:controller].gsub(/^\//, '')
133
+ url_params[:parent_controller] ||= url_params[:controller].gsub(%r{^/}, '')
138
134
  url_params[:controller] = subform_controller.controller_path
139
135
  url_params[:scope] = scope
140
136
  url_params[:parent_id] = params[:parent_id] || params[:id]
@@ -152,8 +148,8 @@ module ActiveScaffold
152
148
  {}
153
149
  end
154
150
 
155
- def render_column(column, record, renders_as, scope = nil, only_value = false, col_class = nil)
156
- if partial = override_form_field_partial(column)
151
+ def render_column(column, record, renders_as, scope = nil, only_value = false, col_class = nil) # rubocop:disable Metrics/ParameterLists
152
+ if (partial = override_form_field_partial(column))
157
153
  render :partial => partial, :locals => {:column => column, :only_value => only_value, :scope => scope, :col_class => col_class, :record => record}
158
154
  elsif renders_as == :field || override_form_field?(column)
159
155
  form_attribute(column, record, scope, only_value, col_class)
@@ -179,17 +175,23 @@ module ActiveScaffold
179
175
  else
180
176
  field = active_scaffold_input_for column, scope, column_options
181
177
  end
178
+ if field
179
+ field << loading_indicator_tag(:action => :render_field, :id => params[:id]) if column.update_columns
180
+ field << content_tag(:span, column.description, :class => 'description') if column.description.present?
181
+ end
182
182
 
183
183
  content_tag :dl, attributes do
184
- %(<dt>#{label_tag label_for(column, column_options), column.label}</dt><dd>#{field}
185
- #{loading_indicator_tag(:action => :render_field, :id => params[:id]) if column.update_columns}
186
- #{content_tag :span, column.description, :class => 'description' if column.description.present?}
187
- </dd>).html_safe
184
+ content_tag(:dt, label_tag(label_for(column, column_options), form_column_label(column))) <<
185
+ content_tag(:dd, field)
188
186
  end
189
187
  end
190
188
 
191
189
  def label_for(column, options)
192
- options[:id] unless column.form_ui == :select && column.association.try(:collection?)
190
+ options[:id] unless column.form_ui == :select && column.association&.collection?
191
+ end
192
+
193
+ def form_column_label(column)
194
+ column.label
193
195
  end
194
196
 
195
197
  def subform_label(column, hidden)
@@ -205,7 +207,7 @@ module ActiveScaffold
205
207
 
206
208
  def form_hidden_field(column, record, scope)
207
209
  options = active_scaffold_input_options(column, scope)
208
- if column.association.try(:collection?)
210
+ if column.association&.collection?
209
211
  associated = record.send(column.name)
210
212
  if associated.blank?
211
213
  hidden_field_tag options[:name], '', options
@@ -217,7 +219,7 @@ module ActiveScaffold
217
219
  safe_join fields, ''
218
220
  end
219
221
  elsif column.association
220
- hidden_field_tag options[:name], record.send(column.name).try(:id), options
222
+ hidden_field_tag options[:name], record.send(column.name)&.id, options
221
223
  else
222
224
  hidden_field :record, column.name, options.merge(object: record)
223
225
  end
@@ -245,7 +247,7 @@ module ActiveScaffold
245
247
  end
246
248
 
247
249
  def column_show_add_existing(column, record = nil)
248
- column.allow_add_existing && options_for_association_count(column.association, record) > 0
250
+ column.allow_add_existing && options_for_association_count(column.association, record).positive?
249
251
  end
250
252
 
251
253
  def column_show_add_new(column, associated, record)
@@ -263,7 +265,7 @@ module ActiveScaffold
263
265
  def active_scaffold_grouped_options(column, select_options, optgroup)
264
266
  group_column = active_scaffold_config_for(column.association.klass).columns[optgroup]
265
267
  group_label = group_column.options[:label_method] if group_column
266
- group_label ||= group_column.try(:association) ? :to_label : :to_s
268
+ group_label ||= group_column&.association ? :to_label : :to_s
267
269
  select_options.group_by(&optgroup.to_sym).collect do |group, options|
268
270
  [group.send(group_label), options.collect { |r| [r.send(column.options[:label_method] || :to_label), r.id] }]
269
271
  end
@@ -276,9 +278,8 @@ module ActiveScaffold
276
278
  end
277
279
 
278
280
  def active_scaffold_select_name_with_multiple(options)
279
- if options[:multiple] && !options[:name].to_s.ends_with?('[]')
280
- options[:name] = "#{options[:name]}[]"
281
- end
281
+ return if !options[:multiple] || options[:name].to_s.ends_with?('[]')
282
+ options[:name] = "#{options[:name]}[]"
282
283
  end
283
284
 
284
285
  def active_scaffold_input_singular_association(column, html_options, options = {})
@@ -289,7 +290,7 @@ module ActiveScaffold
289
290
  select_options.unshift(associated) unless associated.nil? || select_options.include?(associated)
290
291
 
291
292
  method = column.name
292
- options.merge! :selected => associated.try(:id), :include_blank => as_(:_select_), :object => record
293
+ options.merge! :selected => associated&.id, :include_blank => as_(:_select_), :object => record
293
294
 
294
295
  html_options.merge!(column.options[:html_options] || {})
295
296
  options.merge!(column.options)
@@ -303,10 +304,46 @@ module ActiveScaffold
303
304
  collection_select(:record, method, select_options, :id, column.options[:label_method] || :to_label, options, html_options)
304
305
  end
305
306
  html << active_scaffold_refresh_link(column, html_options, record) if column.options[:refresh_link]
307
+ html << active_scaffold_new_record_subform(column, record, html_options) if column.options[:add_new]
306
308
  html
307
309
  end
308
310
 
309
- def active_scaffold_file_with_remove_link(column, options, content, remove_file_prefix, controls_class, &block)
311
+ def active_scaffold_new_record_subform(column, record, html_options, new_record_attributes: nil, locals: {}, skip_link: false) # rubocop:disable Metrics/ParameterLists
312
+ klass =
313
+ if column.association.polymorphic? && column.association.belongs_to?
314
+ type = record.send(column.association.foreign_type)
315
+ column.association.klass(record) if type.present? && (column.options[:add_new] == true || type.in?(column.options[:add_new]))
316
+ else
317
+ column.association.klass
318
+ end
319
+ return content_tag(:div, '') unless klass
320
+ subform_attrs = active_scaffold_subform_attributes(column, nil, klass)
321
+ if record.send(column.name)&.new_record?
322
+ new_record = record.send(column.name)
323
+ else
324
+ subform_attrs[:style] = 'display: none'
325
+ end
326
+ subform_attrs[:class] << ' optional'
327
+ scope = html_options[:name].scan(/record(.*)\[#{column.name}\]/).dig(0, 0)
328
+ new_record ||= klass.new(new_record_attributes)
329
+ locals = locals.reverse_merge(column: column, parent_record: record, associated: [], show_blank_record: new_record, scope: scope)
330
+ subform = render(partial: subform_partial_for_column(column, klass), locals: locals)
331
+ if column.options[:hide_subgroups]
332
+ toggable_id = "#{sub_form_id(association: column.name, id: record.id || generated_id(record) || 99_999_999_999)}-div"
333
+ subform << link_to_visibility_toggle(toggable_id, default_visible: false)
334
+ end
335
+ html = content_tag(:div, subform, subform_attrs)
336
+ return html if skip_link
337
+ html << active_scaffold_show_new_subform_link(column, record, html_options[:id], subform_attrs[:id])
338
+ end
339
+
340
+ def active_scaffold_show_new_subform_link(column, record, select_id, subform_id)
341
+ data = {select_id: select_id, subform_id: subform_id, subform_text: as_(:add_existing), select_text: as_(:create_new)}
342
+ label = data[record.send(column.name)&.new_record? ? :subform_text : :select_text]
343
+ link_to(label, '#', data: data, class: 'show-new-subform')
344
+ end
345
+
346
+ def active_scaffold_file_with_remove_link(column, options, content, remove_file_prefix, controls_class, &block) # rubocop:disable Metrics/ParameterLists
310
347
  options = active_scaffold_input_text_options(options.merge(column.options))
311
348
  if content
312
349
  active_scaffold_file_with_content(column, content, options, remove_file_prefix, controls_class, &block)
@@ -329,12 +366,13 @@ module ActiveScaffold
329
366
  object_name, method = options[:name].split(/\[(#{column.name})\]/)
330
367
  method.sub!(/#{column.name}/, "#{remove_file_prefix}\\0")
331
368
  fields = block_given? ? yield : ''
369
+ link_key = options[:multiple] ? :remove_files : :remove_file
332
370
  input = file_field(:record, column.name, options.merge(:onchange => js_dont_remove_file_code))
333
371
  content_tag(:div, class: controls_class) do
334
372
  content_tag(:div) do
335
373
  safe_join [content, ' | ', fields,
336
374
  hidden_field(object_name, method, :value => 'false', class: 'remove_file'),
337
- content_tag(:a, as_(:remove_file), :href => '#', :onclick => js_remove_file_code)]
375
+ content_tag(:a, as_(link_key), :href => '#', :onclick => js_remove_file_code)]
338
376
  end << content_tag(:div, input, :style => 'display: none')
339
377
  end
340
378
  end
@@ -345,7 +383,7 @@ module ActiveScaffold
345
383
  link_options['data-update_send_form'] = html_options['data-update_send_form']
346
384
  link_options['data-update_send_form_selector'] = html_options['data-update_send_form_selector']
347
385
  else
348
- scope = html_options[:name].scan(/^record((\[[^\]]*\])*)\[#{column.name}\]/)[0].try(:first) if html_options[:name]
386
+ scope = html_options[:name].scan(/^record((\[[^\]]*\])*)\[#{column.name}\]/).dig(0, 0) if html_options[:name]
349
387
  link_options = update_columns_options(column, scope.presence, link_options, true)
350
388
  end
351
389
  link_options[:class] = 'refresh-link'
@@ -385,11 +423,11 @@ module ActiveScaffold
385
423
  label_method = column.options[:label_method] || :to_label
386
424
  html = hidden_field_tag("#{options[:name]}[]", '', :id => nil)
387
425
  html << content_tag(:ul, options.merge(:class => "#{options[:class]} checkbox-list#{' draggable-lists' if column.options[:draggable_lists]}")) do
388
- content = ''.html_safe
426
+ content = []
389
427
  select_options.each_with_index do |option, i|
390
428
  content << active_scaffold_checkbox_option(option, label_method, associated_ids, :name => "#{options[:name]}[]", :id => "#{options[:id]}_#{i}_id")
391
429
  end
392
- content
430
+ safe_join content
393
431
  end
394
432
  html
395
433
  end
@@ -418,9 +456,9 @@ module ActiveScaffold
418
456
  end
419
457
 
420
458
  def active_scaffold_input_select(column, html_options)
421
- if column.association.try :singular?
459
+ if column.association&.singular?
422
460
  active_scaffold_input_singular_association(column, html_options)
423
- elsif column.association.try :collection?
461
+ elsif column.association&.collection?
424
462
  active_scaffold_input_plural_association(column, html_options)
425
463
  else
426
464
  active_scaffold_input_enum(column, html_options)
@@ -430,7 +468,8 @@ module ActiveScaffold
430
468
  def active_scaffold_radio_option(option, selected, column, radio_options)
431
469
  if column.association
432
470
  label_method = column.options[:label_method] || :to_label
433
- text, value = [option.send(label_method), option.id]
471
+ text = option.send(label_method)
472
+ value = option.id
434
473
  checked = {:checked => selected == value}
435
474
  else
436
475
  text, value = active_scaffold_translated_option(column, *option)
@@ -452,11 +491,36 @@ module ActiveScaffold
452
491
  active_scaffold_enum_options(column, record)
453
492
  end
454
493
 
455
- selected = record.send(column.association.name).try(:id) if column.association
456
- radios = options.map do |option|
457
- active_scaffold_radio_option(option, selected, column, html_options)
494
+ selected = record.send(column.association.name) if column.association
495
+ selected_id = selected&.id
496
+ if options.present?
497
+ if column.options[:add_new]
498
+ html_options[:data] ||= {}
499
+ html_options[:data][:subform_id] = active_scaffold_subform_attributes(column)[:id]
500
+ radio_html_options = html_options.merge(class: html_options[:class] + ' hide-new-subform')
501
+ else
502
+ radio_html_options = html_options
503
+ end
504
+ radios = options.map do |option|
505
+ active_scaffold_radio_option(option, selected_id, column, radio_html_options)
506
+ end
507
+ if column.options[:include_blank]
508
+ label = column.options[:include_blank]
509
+ label = as_(column.options[:include_blank]) if column.options[:include_blank].is_a?(Symbol)
510
+ radios.prepend content_tag(:label, radio_button(:record, column.name, '', html_options.merge(id: nil)) + label)
511
+ end
512
+ if column.options[:add_new]
513
+ create_new_button = radio_button_tag(html_options[:name], '', selected&.new_record?, html_options.merge(id: nil, class: html_options[:class] + ' show-new-subform'))
514
+ radios << content_tag(:label, create_new_button << as_(:create_new)) <<
515
+ active_scaffold_new_record_subform(column, record, html_options, skip_link: true)
516
+ end
517
+ safe_join radios
518
+ else
519
+ html = content_tag(:span, as_(:no_options), :class => "#{html_options[:class]} no-options", :id => html_options[:id])
520
+ html << hidden_field_tag(html_options[:name], '', :id => nil)
521
+ html << active_scaffold_new_record_subform(column, record, html_options) if column.options[:add_new]
522
+ html
458
523
  end
459
- safe_join radios
460
524
  end
461
525
 
462
526
  def active_scaffold_input_checkbox(column, options)
@@ -464,7 +528,7 @@ module ActiveScaffold
464
528
  end
465
529
 
466
530
  def active_scaffold_input_password(column, options)
467
- active_scaffold_text_input :password_field, column, options
531
+ active_scaffold_text_input :password_field, column, options.reverse_merge(autocomplete: 'new-password')
468
532
  end
469
533
 
470
534
  def active_scaffold_input_textarea(column, options)
@@ -518,16 +582,17 @@ module ActiveScaffold
518
582
 
519
583
  # A color picker
520
584
  def active_scaffold_input_color(column, options)
585
+ html = []
521
586
  options = active_scaffold_input_text_options(options)
522
- if column.column.try(:null)
587
+ if column.column&.null
523
588
  no_color = options[:object].send(column.name).nil?
524
589
  method = no_color ? :hidden_field : :color_field
525
- html = content_tag(:label, check_box_tag('disable', '1', no_color, id: nil, name: nil, class: 'no-color') << " #{as_ column.options[:no_color] || :no_color}")
590
+ html << content_tag(:label, check_box_tag('disable', '1', no_color, id: nil, name: nil, class: 'no-color') << " #{as_ column.options[:no_color] || :no_color}")
526
591
  else
527
592
  method = :color_field
528
- html = ''.html_safe
529
593
  end
530
594
  html << send(method, :record, column.name, options.merge(column.options).except(:format, :no_color))
595
+ safe_join html
531
596
  end
532
597
 
533
598
  #
@@ -538,8 +603,8 @@ module ActiveScaffold
538
603
  record = options.delete(:object)
539
604
  select_options = []
540
605
  select_options << [as_(:_select_), nil] if !column.virtual? && column.column.null
541
- select_options << [as_(:true), true]
542
- select_options << [as_(:false), false]
606
+ select_options << [as_(:true), true] # rubocop:disable Lint/BooleanSymbol
607
+ select_options << [as_(:false), false] # rubocop:disable Lint/BooleanSymbol
543
608
 
544
609
  select_tag(options[:name], options_for_select(select_options, record.send(column.name)), options)
545
610
  end
@@ -600,8 +665,8 @@ module ActiveScaffold
600
665
  end
601
666
  alias override_input? override_input
602
667
 
603
- def subform_partial_for_column(column)
604
- subform_partial = "#{active_scaffold_config_for(column.association.klass).subform.layout}_subform"
668
+ def subform_partial_for_column(column, klass = nil)
669
+ subform_partial = "#{column.options[:layout] || active_scaffold_config_for(klass || column.association.klass).subform.layout}_subform"
605
670
  override_subform_partial(column, subform_partial) || subform_partial
606
671
  end
607
672
 
@@ -622,7 +687,7 @@ module ActiveScaffold
622
687
  end
623
688
 
624
689
  def column_scope(column, scope = nil, record = nil)
625
- if column.association.try(:collection?)
690
+ if column.association&.collection?
626
691
  "#{scope}[#{column.name}][#{record.id || generate_temporary_id(record)}]"
627
692
  else
628
693
  "#{scope}[#{column.name}]"
@@ -635,12 +700,12 @@ module ActiveScaffold
635
700
  remote_controller = active_scaffold_controller_for(record_select_config.model).controller_path
636
701
  options[:controller] = remote_controller
637
702
  options.merge!(active_scaffold_input_text_options)
638
- record_select_field(options[:name], record, options)
703
+ record_select_field(options[:name], nil, options)
639
704
  else
640
705
  select_options = sorted_association_options_find(nested.association, nil, record)
641
706
  select_options ||= active_scaffold_config.model.all
642
707
  select_options = options_from_collection_for_select(select_options, :id, :to_label)
643
- select_tag 'associated_id', ('<option value="">' + as_(:_select_) + '</option>' + select_options).html_safe unless select_options.empty?
708
+ select_tag 'associated_id', (content_tag(:option, as_(:_select_), value: '') + select_options) unless select_options.empty?
644
709
  end
645
710
  end
646
711
 
@@ -653,55 +718,62 @@ module ActiveScaffold
653
718
  end
654
719
 
655
720
  # Try to get numerical constraints from model's validators
656
- def numerical_constraints_for_column(column, options)
657
- if column.numerical_constraints.nil?
658
- numerical_constraints = {}
659
- validators = column.active_record_class.validators.select do |v|
660
- v.is_a?(ActiveModel::Validations::NumericalityValidator) && v.attributes.include?(column.name)
661
- end
662
- equal_to = (val = validators.find { |v| v.options[:equal_to] }) ? val.options[:equal_to] : nil
663
-
664
- # If there is equal_to constraint - use it (unless otherwise specified by user)
665
- if equal_to && !(options[:min] || options[:max])
666
- numerical_constraints[:min] = numerical_constraints[:max] = equal_to
667
- else # find minimum and maximum from validators
668
- # we can safely modify :min and :max by 1 for :greater_tnan or :less_than value only for integer values
669
- only_integer = column.column.type == :integer if column.column
670
- only_integer ||= validators.find { |v| v.options[:only_integer] }.present?
671
- margin = only_integer ? 1 : 0
672
-
673
- # Minimum
674
- unless options[:min]
675
- min = validators.map { |v| v.options[:greater_than_or_equal] }.compact.max
676
- greater_than = validators.map { |v| v.options[:greater_than] }.compact.max
677
- numerical_constraints[:min] = [min, (greater_than + margin if greater_than)].compact.max
678
- end
721
+ def column_numerical_constraints(column, options)
722
+ validators = column.active_record_class.validators.select do |v|
723
+ v.is_a?(ActiveModel::Validations::NumericalityValidator) && v.attributes.include?(column.name)
724
+ end
679
725
 
680
- # Maximum
681
- unless options[:max]
682
- max = validators.map { |v| v.options[:less_than_or_equal] }.compact.min
683
- less_than = validators.map { |v| v.options[:less_than] }.compact.min
684
- numerical_constraints[:max] = [max, (less_than - margin if less_than)].compact.min
685
- end
726
+ equal_validator = validators.find { |v| v.options[:equal_to] }
727
+ # If there is equal_to constraint - use it (unless otherwise specified by user)
728
+ if equal_validator && !(options[:min] || options[:max])
729
+ equal_to = equal_validator.options[:equal_to]
730
+ return {min: equal_to, max: equal_to}
731
+ end
686
732
 
687
- # Set step = 2 for column values restricted to be odd or even (but only if minimum is set)
688
- unless options[:step]
689
- only_odd_valid = validators.any? { |v| v.options[:odd] }
690
- only_even_valid = validators.any? { |v| v.options[:even] } unless only_odd_valid
691
- if !only_integer
692
- numerical_constraints[:step] ||= "0.#{'0' * (column.column.scale - 1)}1" if column.column && column.column.scale.to_i > 0
693
- elsif options[:min] && options[:min].respond_to?(:even?) && (only_odd_valid || only_even_valid)
694
- numerical_constraints[:step] = 2
695
- numerical_constraints[:min] += 1 if only_odd_valid && options[:min].even?
696
- numerical_constraints[:min] += 1 if only_even_valid && options[:min].odd?
697
- end
698
- numerical_constraints[:step] ||= 'any' unless only_integer
699
- end
733
+ numerical_constraints = {}
734
+
735
+ # find minimum and maximum from validators
736
+ # we can safely modify :min and :max by 1 for :greater_tnan or :less_than value only for integer values
737
+ only_integer = column.column.type == :integer if column.column
738
+ only_integer ||= validators.find { |v| v.options[:only_integer] }.present?
739
+ margin = only_integer ? 1 : 0
740
+
741
+ # Minimum
742
+ unless options[:min]
743
+ min = validators.map { |v| v.options[:greater_than_or_equal_to] }.compact.max
744
+ greater_than = validators.map { |v| v.options[:greater_than] }.compact.max
745
+ numerical_constraints[:min] = [min, (greater_than + margin if greater_than)].compact.max
746
+ end
747
+
748
+ # Maximum
749
+ unless options[:max]
750
+ max = validators.map { |v| v.options[:less_than_or_equal_to] }.compact.min
751
+ less_than = validators.map { |v| v.options[:less_than] }.compact.min
752
+ numerical_constraints[:max] = [max, (less_than - margin if less_than)].compact.min
753
+ end
754
+
755
+ # Set step = 2 for column values restricted to be odd or even (but only if minimum is set)
756
+ unless options[:step]
757
+ only_odd_valid = validators.any? { |v| v.options[:odd] }
758
+ only_even_valid = validators.any? { |v| v.options[:even] } unless only_odd_valid
759
+ if !only_integer
760
+ numerical_constraints[:step] ||= "0.#{'0' * (column.column.scale - 1)}1" if column.column&.scale.to_i.positive?
761
+ elsif options[:min] && options[:min].respond_to?(:even?) && (only_odd_valid || only_even_valid)
762
+ numerical_constraints[:step] = 2
763
+ numerical_constraints[:min] += 1 if only_odd_valid && options[:min].even?
764
+ numerical_constraints[:min] += 1 if only_even_valid && options[:min].odd?
700
765
  end
766
+ numerical_constraints[:step] ||= 'any' unless only_integer
767
+ end
701
768
 
702
- column.numerical_constraints = numerical_constraints
769
+ numerical_constraints
770
+ end
771
+
772
+ def numerical_constraints_for_column(column, options)
773
+ constraints = Rails.cache.fetch("#{column.cache_key}#numerical_constarints") do
774
+ column_numerical_constraints(column, options)
703
775
  end
704
- column.numerical_constraints.merge(options)
776
+ constraints.merge(options)
705
777
  end
706
778
  end
707
779
  end