rails_admin 3.1.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +20 -15
  3. data/README.md +2 -2
  4. data/app/assets/javascripts/rails_admin/application.js.erb +3 -2
  5. data/app/assets/stylesheets/rails_admin/application.scss.erb +1 -1
  6. data/app/controllers/rails_admin/main_controller.rb +5 -1
  7. data/app/helpers/rails_admin/application_helper.rb +4 -0
  8. data/app/helpers/rails_admin/form_builder.rb +2 -2
  9. data/app/helpers/rails_admin/main_helper.rb +1 -1
  10. data/app/views/layouts/rails_admin/_head.html.erb +10 -7
  11. data/app/views/rails_admin/main/_form_boolean.html.erb +2 -2
  12. data/app/views/rails_admin/main/_form_filtering_multiselect.html.erb +5 -35
  13. data/app/views/rails_admin/main/_form_filtering_select.html.erb +6 -18
  14. data/app/views/rails_admin/main/_form_nested_many.html.erb +1 -1
  15. data/app/views/rails_admin/main/_form_nested_one.html.erb +1 -1
  16. data/app/views/rails_admin/main/_form_polymorphic_association.html.erb +12 -21
  17. data/app/views/rails_admin/main/delete.html.erb +1 -1
  18. data/app/views/rails_admin/main/index.html.erb +2 -2
  19. data/config/initializers/active_record_extensions.rb +0 -23
  20. data/lib/generators/rails_admin/importmap_formatter.rb +1 -1
  21. data/lib/generators/rails_admin/install_generator.rb +13 -1
  22. data/lib/generators/rails_admin/templates/rails_admin.vite.js +2 -0
  23. data/lib/rails_admin/abstract_model.rb +18 -7
  24. data/lib/rails_admin/adapters/active_record/association.rb +27 -10
  25. data/lib/rails_admin/adapters/active_record/object_extension.rb +0 -18
  26. data/lib/rails_admin/adapters/active_record.rb +52 -5
  27. data/lib/rails_admin/adapters/active_record.rb.bak +348 -0
  28. data/lib/rails_admin/adapters/mongoid/association.rb +3 -3
  29. data/lib/rails_admin/adapters/mongoid/bson.rb +1 -0
  30. data/lib/rails_admin/adapters/mongoid/object_extension.rb +0 -5
  31. data/lib/rails_admin/adapters/mongoid.rb +8 -3
  32. data/lib/rails_admin/config/actions/index.rb +5 -3
  33. data/lib/rails_admin/config/fields/association.rb +41 -2
  34. data/lib/rails_admin/config/fields/base.rb +5 -5
  35. data/lib/rails_admin/config/fields/collection_association.rb +90 -0
  36. data/lib/rails_admin/config/fields/factories/active_storage.rb +2 -2
  37. data/lib/rails_admin/config/fields/factories/carrierwave.rb +1 -1
  38. data/lib/rails_admin/config/fields/factories/dragonfly.rb +1 -1
  39. data/lib/rails_admin/config/fields/factories/paperclip.rb +1 -1
  40. data/lib/rails_admin/config/fields/factories/shrine.rb +1 -1
  41. data/lib/rails_admin/config/fields/singular_association.rb +59 -0
  42. data/lib/rails_admin/config/fields/types/active_storage.rb +12 -7
  43. data/lib/rails_admin/config/fields/types/all.rb +0 -1
  44. data/lib/rails_admin/config/fields/types/belongs_to_association.rb +17 -20
  45. data/lib/rails_admin/config/fields/types/dragonfly.rb +0 -1
  46. data/lib/rails_admin/config/fields/types/file_upload.rb +7 -1
  47. data/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb +2 -2
  48. data/lib/rails_admin/config/fields/types/has_many_association.rb +2 -24
  49. data/lib/rails_admin/config/fields/types/has_one_association.rb +12 -22
  50. data/lib/rails_admin/config/fields/types/multiple_active_storage.rb +13 -8
  51. data/lib/rails_admin/config/fields/types/multiple_file_upload.rb +7 -1
  52. data/lib/rails_admin/config/fields/types/polymorphic_association.rb +32 -9
  53. data/lib/rails_admin/config.rb +5 -0
  54. data/lib/rails_admin/engine.rb +5 -0
  55. data/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb +1 -1
  56. data/lib/rails_admin/extensions/url_for_extension.rb +15 -0
  57. data/lib/rails_admin/support/composite_keys_serializer.rb +15 -0
  58. data/lib/rails_admin/support/datetime.rb +1 -0
  59. data/lib/rails_admin/version.rb +2 -2
  60. data/package.json +2 -2
  61. data/src/rails_admin/abstract-select.js +30 -0
  62. data/src/rails_admin/base.js +4 -1
  63. data/src/rails_admin/filtering-multiselect.js +2 -4
  64. data/src/rails_admin/filtering-select.js +2 -4
  65. metadata +41 -16
  66. data/lib/rails_admin/adapters/composite_primary_keys/association.rb +0 -45
  67. data/lib/rails_admin/adapters/composite_primary_keys.rb +0 -40
  68. data/lib/rails_admin/config/fields/types/composite_keys_belongs_to_association.rb +0 -31
@@ -15,7 +15,7 @@ module RailsAdmin
15
15
  end
16
16
 
17
17
  def get(id, scope = scoped)
18
- object = scope.where(primary_key => id).first
18
+ object = primary_key_scope(scope, id).first
19
19
  return unless object
20
20
 
21
21
  object.extend(ObjectExtension)
@@ -72,6 +72,14 @@ module RailsAdmin
72
72
 
73
73
  delegate :primary_key, :table_name, to: :model, prefix: false
74
74
 
75
+ def quoted_table_name
76
+ model.quoted_table_name
77
+ end
78
+
79
+ def quote_column_name(name)
80
+ model.connection.quote_column_name(name)
81
+ end
82
+
75
83
  def encoding
76
84
  adapter =
77
85
  if ::ActiveRecord::Base.respond_to?(:connection_db_config)
@@ -107,10 +115,42 @@ module RailsAdmin
107
115
  true
108
116
  end
109
117
 
118
+ def format_id(id)
119
+ if primary_key.is_a? Array
120
+ RailsAdmin.config.composite_keys_serializer.serialize(id)
121
+ else
122
+ id
123
+ end
124
+ end
125
+
126
+ def parse_id(id)
127
+ if primary_key.is_a?(Array)
128
+ ids = RailsAdmin.config.composite_keys_serializer.deserialize(id)
129
+ primary_key.each_with_index do |key, i|
130
+ ids[i] = model.type_for_attribute(key).cast(ids[i])
131
+ end
132
+ ids
133
+ else
134
+ id
135
+ end
136
+ end
137
+
110
138
  private
111
139
 
140
+ def primary_key_scope(scope, id)
141
+ if primary_key.is_a? Array
142
+ scope.where(primary_key.zip(parse_id(id)).to_h)
143
+ else
144
+ scope.where(primary_key => id)
145
+ end
146
+ end
147
+
112
148
  def bulk_scope(scope, options)
113
- scope.where(primary_key => options[:bulk_ids])
149
+ if primary_key.is_a? Array
150
+ options[:bulk_ids].map { |id| primary_key_scope(scope, id) }.reduce(&:or)
151
+ else
152
+ scope.where(primary_key => options[:bulk_ids])
153
+ end
114
154
  end
115
155
 
116
156
  def sort_scope(scope, options)
@@ -172,7 +212,7 @@ module RailsAdmin
172
212
  # "0055" is the filter index, no use here. o is the operator, v the value
173
213
  def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?))
174
214
  filters.each_pair do |field_name, filters_dump|
175
- filters_dump.each do |_, filter_dump|
215
+ filters_dump.each_value do |filter_dump|
176
216
  wb = WhereBuilder.new(scope)
177
217
  field = fields.detect { |f| f.name.to_s == field_name }
178
218
  value = parse_field_value(field, filter_dump[:v])
@@ -201,6 +241,8 @@ module RailsAdmin
201
241
  case @type
202
242
  when :boolean
203
243
  boolean_unary_operators
244
+ when :uuid
245
+ uuid_unary_operators
204
246
  when :integer, :decimal, :float
205
247
  numeric_unary_operators
206
248
  else
@@ -230,6 +272,7 @@ module RailsAdmin
230
272
  )
231
273
  end
232
274
  alias_method :numeric_unary_operators, :boolean_unary_operators
275
+ alias_method :uuid_unary_operators, :boolean_unary_operators
233
276
 
234
277
  def range_filter(min, max)
235
278
  if min && max && min == max
@@ -255,8 +298,12 @@ module RailsAdmin
255
298
  end
256
299
 
257
300
  def build_statement_for_boolean
258
- return ["(#{@column} IS NULL OR #{@column} = ?)", false] if %w[false f 0].include?(@value)
259
- return ["(#{@column} = ?)", true] if %w[true t 1].include?(@value)
301
+ case @value
302
+ when 'false', 'f', '0'
303
+ ["(#{@column} IS NULL OR #{@column} = ?)", false]
304
+ when 'true', 't', '1'
305
+ ["(#{@column} = ?)", true]
306
+ end
260
307
  end
261
308
 
262
309
  def column_for_value(value)
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'rails_admin/adapters/active_record/association'
5
+ require 'rails_admin/adapters/active_record/model_extension'
6
+ require 'rails_admin/adapters/active_record/property'
7
+
8
+ module RailsAdmin
9
+ module Adapters
10
+ module ActiveRecord
11
+ DISABLED_COLUMN_TYPES = %i[tsvector blob binary spatial hstore geometry].freeze
12
+
13
+ def model_with_extension
14
+ @model_with_extension ||=
15
+ begin
16
+ klass = Class.new(model) do
17
+ include ModelExtension
18
+ end
19
+ klass.instance_eval <<-RUBY, __FILE__, __LINE__+1
20
+ def name
21
+ "#{@model_name.to_s}"
22
+ end
23
+ RUBY
24
+ klass
25
+ end
26
+ end
27
+
28
+ def new(params = {})
29
+ model_with_extension.new(params)
30
+ end
31
+
32
+ def get(id, scope = nil)
33
+ scope = model_with_extension.merge(scope || scoped)
34
+ scope.where(primary_key => id).first
35
+ end
36
+
37
+ def scoped
38
+ model_with_extension.all
39
+ end
40
+
41
+ def first(options = {}, scope = nil)
42
+ all(options, scope).first
43
+ end
44
+
45
+ def all(options = {}, scope = nil)
46
+ scope = model_with_extension.merge(scope || scoped)
47
+ scope = scope.includes(options[:include]) if options[:include]
48
+ scope = scope.limit(options[:limit]) if options[:limit]
49
+ scope = bulk_scope(scope, options) if options[:bulk_ids]
50
+ scope = query_scope(scope, options[:query]) if options[:query]
51
+ scope = filter_scope(scope, options[:filters]) if options[:filters]
52
+ scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per]) if options[:page] && options[:per]
53
+ scope = sort_scope(scope, options) if options[:sort]
54
+ scope
55
+ end
56
+
57
+ def count(options = {}, scope = nil)
58
+ all(options.merge(limit: false, page: false), scope).count(:all)
59
+ end
60
+
61
+ def destroy(objects)
62
+ Array.wrap(objects).each(&:destroy)
63
+ end
64
+
65
+ def associations
66
+ model.reflect_on_all_associations.collect do |association|
67
+ Association.new(association, model)
68
+ end
69
+ end
70
+
71
+ def properties
72
+ columns = model.columns.reject do |c|
73
+ c.type.blank? ||
74
+ DISABLED_COLUMN_TYPES.include?(c.type.to_sym) ||
75
+ c.try(:array)
76
+ end
77
+ columns.collect do |property|
78
+ Property.new(property, model)
79
+ end
80
+ end
81
+
82
+ def base_class
83
+ model.base_class
84
+ end
85
+
86
+ delegate :primary_key, :table_name, to: :model, prefix: false
87
+
88
+ def quoted_table_name
89
+ model.quoted_table_name
90
+ end
91
+
92
+ def quote_column_name(name)
93
+ model.connection.quote_column_name(name)
94
+ end
95
+
96
+ def encoding
97
+ adapter =
98
+ if ::ActiveRecord::Base.respond_to?(:connection_db_config)
99
+ ::ActiveRecord::Base.connection_db_config.configuration_hash[:adapter]
100
+ else
101
+ ::ActiveRecord::Base.connection_config[:adapter]
102
+ end
103
+ case adapter
104
+ when 'postgresql'
105
+ ::ActiveRecord::Base.connection.select_one("SELECT ''::text AS str;").values.first.encoding
106
+ when 'mysql2'
107
+ if RUBY_ENGINE == 'jruby'
108
+ ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding
109
+ else
110
+ ::ActiveRecord::Base.connection.raw_connection.encoding
111
+ end
112
+ when 'oracle_enhanced'
113
+ ::ActiveRecord::Base.connection.select_one('SELECT dummy FROM DUAL').values.first.encoding
114
+ else
115
+ ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding
116
+ end
117
+ end
118
+
119
+ def embedded?
120
+ false
121
+ end
122
+
123
+ def cyclic?
124
+ false
125
+ end
126
+
127
+ def adapter_supports_joins?
128
+ true
129
+ end
130
+
131
+ private
132
+
133
+ def bulk_scope(scope, options)
134
+ scope.where(primary_key => options[:bulk_ids])
135
+ end
136
+
137
+ def sort_scope(scope, options)
138
+ direction = options[:sort_reverse] ? :asc : :desc
139
+ case options[:sort]
140
+ when String, Symbol
141
+ scope.reorder("#{options[:sort]} #{direction}")
142
+ when Array
143
+ scope.reorder(options[:sort].zip(Array.new(options[:sort].size) { direction }).to_h)
144
+ when Hash
145
+ scope.reorder(options[:sort].map { |table_name, column| "#{table_name}.#{column}" }.
146
+ zip(Array.new(options[:sort].size) { direction }).to_h)
147
+ else
148
+ raise ArgumentError.new("Unsupported sort value: #{options[:sort]}")
149
+ end
150
+ end
151
+
152
+ class WhereBuilder
153
+ def initialize(scope)
154
+ @statements = []
155
+ @values = []
156
+ @tables = []
157
+ @scope = scope
158
+ end
159
+
160
+ def add(field, value, operator)
161
+ field.searchable_columns.flatten.each do |column_infos|
162
+ statement, value1, value2 = StatementBuilder.new(column_infos[:column], column_infos[:type], value, operator, @scope.connection.adapter_name).to_statement
163
+ @statements << statement if statement.present?
164
+ @values << value1 unless value1.nil?
165
+ @values << value2 unless value2.nil?
166
+ table, column = column_infos[:column].split('.')
167
+ @tables.push(table) if column
168
+ end
169
+ end
170
+
171
+ def build
172
+ scope = @scope.where(@statements.join(' OR '), *@values)
173
+ scope = scope.references(*@tables.uniq) if @tables.any?
174
+ scope
175
+ end
176
+ end
177
+
178
+ def query_scope(scope, query, fields = config.list.fields.select(&:queryable?))
179
+ if config.list.search_by
180
+ scope.send(config.list.search_by, query)
181
+ else
182
+ wb = WhereBuilder.new(scope)
183
+ fields.each do |field|
184
+ value = parse_field_value(field, query)
185
+ wb.add(field, value, field.search_operator)
186
+ end
187
+ # OR all query statements
188
+ wb.build
189
+ end
190
+ end
191
+
192
+ # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...}
193
+ # "0055" is the filter index, no use here. o is the operator, v the value
194
+ def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?))
195
+ filters.each_pair do |field_name, filters_dump|
196
+ filters_dump.each_value do |filter_dump|
197
+ wb = WhereBuilder.new(scope)
198
+ field = fields.detect { |f| f.name.to_s == field_name }
199
+ value = parse_field_value(field, filter_dump[:v])
200
+
201
+ wb.add(field, value, (filter_dump[:o] || RailsAdmin::Config.default_search_operator))
202
+ # AND current filter statements to other filter statements
203
+ scope = wb.build
204
+ end
205
+ end
206
+ scope
207
+ end
208
+
209
+ def build_statement(column, type, value, operator)
210
+ StatementBuilder.new(column, type, value, operator, model.connection.adapter_name).to_statement
211
+ end
212
+
213
+ class StatementBuilder < RailsAdmin::AbstractModel::StatementBuilder
214
+ def initialize(column, type, value, operator, adapter_name)
215
+ super column, type, value, operator
216
+ @adapter_name = adapter_name
217
+ end
218
+
219
+ protected
220
+
221
+ def unary_operators
222
+ case @type
223
+ when :boolean
224
+ boolean_unary_operators
225
+ when :uuid
226
+ uuid_unary_operators
227
+ when :integer, :decimal, :float
228
+ numeric_unary_operators
229
+ else
230
+ generic_unary_operators
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def generic_unary_operators
237
+ {
238
+ '_blank' => ["(#{@column} IS NULL OR #{@column} = '')"],
239
+ '_present' => ["(#{@column} IS NOT NULL AND #{@column} != '')"],
240
+ '_null' => ["(#{@column} IS NULL)"],
241
+ '_not_null' => ["(#{@column} IS NOT NULL)"],
242
+ '_empty' => ["(#{@column} = '')"],
243
+ '_not_empty' => ["(#{@column} != '')"],
244
+ }
245
+ end
246
+
247
+ def boolean_unary_operators
248
+ generic_unary_operators.merge(
249
+ '_blank' => ["(#{@column} IS NULL)"],
250
+ '_empty' => ["(#{@column} IS NULL)"],
251
+ '_present' => ["(#{@column} IS NOT NULL)"],
252
+ '_not_empty' => ["(#{@column} IS NOT NULL)"],
253
+ )
254
+ end
255
+ alias_method :numeric_unary_operators, :boolean_unary_operators
256
+ alias_method :uuid_unary_operators, :boolean_unary_operators
257
+
258
+ def range_filter(min, max)
259
+ if min && max && min == max
260
+ ["(#{@column} = ?)", min]
261
+ elsif min && max
262
+ ["(#{@column} BETWEEN ? AND ?)", min, max]
263
+ elsif min
264
+ ["(#{@column} >= ?)", min]
265
+ elsif max
266
+ ["(#{@column} <= ?)", max]
267
+ end
268
+ end
269
+
270
+ def build_statement_for_type
271
+ case @type
272
+ when :boolean then build_statement_for_boolean
273
+ when :integer, :decimal, :float then build_statement_for_integer_decimal_or_float
274
+ when :string, :text, :citext then build_statement_for_string_or_text
275
+ when :enum then build_statement_for_enum
276
+ when :belongs_to_association then build_statement_for_belongs_to_association
277
+ when :uuid then build_statement_for_uuid
278
+ end
279
+ end
280
+
281
+ def build_statement_for_boolean
282
+ case @value
283
+ when 'false', 'f', '0'
284
+ ["(#{@column} IS NULL OR #{@column} = ?)", false]
285
+ when 'true', 't', '1'
286
+ ["(#{@column} = ?)", true]
287
+ end
288
+ end
289
+
290
+ def column_for_value(value)
291
+ ["(#{@column} = ?)", value]
292
+ end
293
+
294
+ def build_statement_for_belongs_to_association
295
+ return if @value.blank?
296
+
297
+ ["(#{@column} = ?)", @value.to_i] if @value.to_i.to_s == @value
298
+ end
299
+
300
+ def build_statement_for_string_or_text
301
+ return if @value.blank?
302
+
303
+ return ["(#{@column} = ?)", @value] if ['is', '='].include?(@operator)
304
+
305
+ @value = @value.mb_chars.downcase unless %w[postgresql postgis].include? ar_adapter
306
+
307
+ @value =
308
+ case @operator
309
+ when 'default', 'like', 'not_like'
310
+ "%#{@value}%"
311
+ when 'starts_with'
312
+ "#{@value}%"
313
+ when 'ends_with'
314
+ "%#{@value}"
315
+ else
316
+ return
317
+ end
318
+
319
+ if %w[postgresql postgis].include? ar_adapter
320
+ if @operator == 'not_like'
321
+ ["(#{@column} NOT ILIKE ?)", @value]
322
+ else
323
+ ["(#{@column} ILIKE ?)", @value]
324
+ end
325
+ elsif @operator == 'not_like'
326
+ ["(LOWER(#{@column}) NOT LIKE ?)", @value]
327
+ else
328
+ ["(LOWER(#{@column}) LIKE ?)", @value]
329
+ end
330
+ end
331
+
332
+ def build_statement_for_enum
333
+ return if @value.blank?
334
+
335
+ ["(#{@column} IN (?))", Array.wrap(@value)]
336
+ end
337
+
338
+ def build_statement_for_uuid
339
+ column_for_value(@value) if /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/.match?(@value.to_s)
340
+ end
341
+
342
+ def ar_adapter
343
+ @adapter_name.downcase
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
@@ -46,7 +46,7 @@ module RailsAdmin
46
46
 
47
47
  def klass
48
48
  if polymorphic? && %i[referenced_in belongs_to].include?(macro)
49
- polymorphic_parents(:mongoid, model.name, name) || []
49
+ polymorphic_parents(:mongoid, association.inverse_class_name, name) || []
50
50
  else
51
51
  association.klass
52
52
  end
@@ -92,9 +92,9 @@ module RailsAdmin
92
92
  def key_accessor
93
93
  case macro.to_sym
94
94
  when :has_many
95
- "#{name.to_s.singularize}_ids".to_sym
95
+ :"#{name.to_s.singularize}_ids"
96
96
  when :has_one
97
- "#{name}_id".to_sym
97
+ :"#{name}_id"
98
98
  when :embedded_in, :embeds_one, :embeds_many
99
99
  nil
100
100
  else
@@ -21,6 +21,7 @@ module RailsAdmin
21
21
  Moped::Errors::InvalidObjectId
22
22
  BSON::ObjectId::Invalid
23
23
  BSON::InvalidObjectId
24
+ BSON::Error::InvalidObjectId
24
25
  ].exclude?(e.class.to_s)
25
26
  end
26
27
  end
@@ -20,11 +20,6 @@ module RailsAdmin
20
20
  send(name)&.save
21
21
  end
22
22
  end
23
- object.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
24
- def #{name}_id=(item_id)
25
- self.#{name} = (#{association.klass}.find(item_id) rescue nil)
26
- end
27
- RUBY
28
23
  end
29
24
  end
30
25
  end
@@ -31,6 +31,7 @@ module RailsAdmin
31
31
  Mongoid::Errors::InvalidFind
32
32
  Moped::Errors::InvalidObjectId
33
33
  BSON::InvalidObjectId
34
+ BSON::Error::InvalidObjectId
34
35
  ].exclude?(e.class.to_s)
35
36
  end
36
37
 
@@ -152,7 +153,7 @@ module RailsAdmin
152
153
  statements = []
153
154
 
154
155
  filters.each_pair do |field_name, filters_dump|
155
- filters_dump.each do |_, filter_dump|
156
+ filters_dump.each_value do |filter_dump|
156
157
  field = fields.detect { |f| f.name.to_s == field_name }
157
158
  next unless field
158
159
 
@@ -250,8 +251,12 @@ module RailsAdmin
250
251
  end
251
252
 
252
253
  def build_statement_for_boolean
253
- return {@column => false} if %w[false f 0].include?(@value)
254
- return {@column => true} if %w[true t 1].include?(@value)
254
+ case @value
255
+ when 'false', 'f', '0'
256
+ {@column => false}
257
+ when 'true', 't', '1'
258
+ {@column => true}
259
+ end
255
260
  end
256
261
 
257
262
  def column_for_value(value)
@@ -50,9 +50,11 @@ module RailsAdmin
50
50
  format.json do
51
51
  output =
52
52
  if params[:compact]
53
- primary_key_method = @association ? @association.associated_primary_key : @model_config.abstract_model.primary_key
54
- label_method = @model_config.object_label_method
55
- @objects.collect { |o| {id: o.send(primary_key_method).to_s, label: o.send(label_method).to_s} }
53
+ if @association
54
+ @association.collection(@objects).collect { |(label, id)| {id: id, label: label} }
55
+ else
56
+ @objects.collect { |object| {id: object.id.to_s, label: object.send(@model_config.object_label_method).to_s} }
57
+ end
56
58
  else
57
59
  @objects.to_json(@schema)
58
60
  end
@@ -13,7 +13,7 @@ module RailsAdmin
13
13
  end
14
14
 
15
15
  def method_name
16
- association.key_accessor
16
+ nested_form ? :"#{name}_attributes" : association.key_accessor
17
17
  end
18
18
 
19
19
  register_instance_option :pretty_value do
@@ -56,7 +56,30 @@ module RailsAdmin
56
56
  # preload entire associated collection (per associated_collection_scope) on load
57
57
  # Be sure to set limit in associated_collection_scope if set is large
58
58
  register_instance_option :associated_collection_cache_all do
59
- @associated_collection_cache_all ||= (associated_model_config.abstract_model.count < associated_model_limit)
59
+ @associated_collection_cache_all ||= dynamically_scope_by.blank? && (associated_model_config.abstract_model.count < associated_model_limit)
60
+ end
61
+
62
+ # client-side dynamic scoping
63
+ register_instance_option :dynamically_scope_by do
64
+ nil
65
+ end
66
+
67
+ # parses #dynamically_scope_by and returns a Hash in the form of
68
+ # {[form field name in this model]: [field name in the associated model]}
69
+ def dynamic_scope_relationships
70
+ @dynamic_scope_relationships ||=
71
+ Array.wrap(dynamically_scope_by).flat_map do |field|
72
+ field.is_a?(Hash) ? field.to_a : [[field, field]]
73
+ end.map do |field_name, target_name| # rubocop:disable Style/MultilineBlockChain
74
+ field = section.fields.detect { |f| f.name == field_name }
75
+ raise "Field '#{field_name}' was given for #dynamically_scope_by but not found in '#{abstract_model.model_name}'" unless field
76
+
77
+ target_field = associated_model_config.list.fields.detect { |f| f.name == target_name }
78
+ raise "Field '#{field_name}' was given for #dynamically_scope_by but not found in '#{associated_model_config.abstract_model.model_name}'" unless target_field
79
+ raise "Field '#{field_name}' in '#{associated_model_config.abstract_model.model_name}' can't be used for dynamic scoping because it's not filterable" unless target_field.filterable
80
+
81
+ [field.method_name, target_name]
82
+ end.to_h
60
83
  end
61
84
 
62
85
  # determines whether association's elements can be removed
@@ -111,6 +134,12 @@ module RailsAdmin
111
134
  bindings[:object].send(association.name)
112
135
  end
113
136
 
137
+ # Returns collection of all selectable records
138
+ def collection(scope = nil)
139
+ (scope || bindings[:controller].list_entries(associated_model_config, :index, associated_collection_scope, false)).
140
+ map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key)).to_s] }
141
+ end
142
+
114
143
  # has many?
115
144
  def multiple?
116
145
  true
@@ -123,6 +152,16 @@ module RailsAdmin
123
152
  def associated_model_limit
124
153
  RailsAdmin.config.default_associated_collection_limit
125
154
  end
155
+
156
+ private
157
+
158
+ def format_key(key)
159
+ if key.is_a?(Array)
160
+ RailsAdmin.config.composite_keys_serializer.serialize(key)
161
+ else
162
+ key
163
+ end
164
+ end
126
165
  end
127
166
  end
128
167
  end
@@ -62,15 +62,15 @@ module RailsAdmin
62
62
 
63
63
  def sort_column
64
64
  if sortable == true
65
- "#{abstract_model.table_name}.#{name}"
65
+ "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(name)}"
66
66
  elsif (sortable.is_a?(String) || sortable.is_a?(Symbol)) && sortable.to_s.include?('.') # just provide sortable, don't do anything smart
67
67
  sortable
68
68
  elsif sortable.is_a?(Hash) # just join sortable hash, don't do anything smart
69
69
  "#{sortable.keys.first}.#{sortable.values.first}"
70
- elsif association # use column on target table
71
- "#{associated_model_config.abstract_model.table_name}.#{sortable}"
70
+ elsif association? # use column on target table
71
+ "#{associated_model_config.abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}"
72
72
  else # use described column in the field conf.
73
- "#{abstract_model.table_name}.#{sortable}"
73
+ "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}"
74
74
  end
75
75
  end
76
76
 
@@ -357,7 +357,7 @@ module RailsAdmin
357
357
 
358
358
  def generic_field_help
359
359
  model = abstract_model.model_name.underscore
360
- model_lookup = "admin.help.#{model}.#{name}".to_sym
360
+ model_lookup = :"admin.help.#{model}.#{name}"
361
361
  translated = I18n.translate(model_lookup, help: generic_help, default: [generic_help])
362
362
  (translated.is_a?(Hash) ? translated.to_a.first[1] : translated).html_safe
363
363
  end