active_scaffold 3.5.4 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG → CHANGELOG.rdoc} +72 -0
  3. data/README.md +20 -8
  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 +9 -7
  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 +9 -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 +29 -34
  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 +82 -66
  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 +2 -2
  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 +69 -18
  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