rails_admin 2.2.0 → 2.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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/rails_admin/ra.filter-box.js +8 -0
- data/app/views/rails_admin/main/index.html.haml +1 -1
- data/config/locales/rails_admin.en.yml +1 -0
- data/lib/rails_admin/adapters/active_record.rb +8 -2
- data/lib/rails_admin/adapters/active_record.rb.bak +348 -0
- data/lib/rails_admin/adapters/mongoid.rb +2 -0
- data/lib/rails_admin/config.rb +1 -1
- data/lib/rails_admin/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af37bc66a94584a21b754a3832c8556484f2e8fef1b5b926e373b11ae9ca7ff9
|
4
|
+
data.tar.gz: a50edd522f580bfcad1d574f5142c678c238acf9c56f269194cbd89b1255cec4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa60983825f2d6ebca5c5b940a9e849cc491bb51981b1ee07a7b90cf6038167e4ac31823b49b7b0256d0f293aedaacf6cb0248e1ce038e7a03b72f5c3d70d8f9
|
7
|
+
data.tar.gz: 95f6c62c0d8dafe6ff4359992df55b1dde343fdf8a6ff4222d07f263d94cf1ddf1710a576ff83c6fb2cb25cac3edb566134dea9374f4b55a5e61ad673d93f47b
|
@@ -98,6 +98,7 @@
|
|
98
98
|
$('<option value="_blank"></option>').prop('selected', field_value == "_blank").text(RailsAdmin.I18n.t("is_blank")),
|
99
99
|
'<option disabled="disabled">---------</option>'
|
100
100
|
])
|
101
|
+
.append(select_options)
|
101
102
|
.add(
|
102
103
|
$('<select multiple="multiple" class="select-multiple input-sm form-control"></select>')
|
103
104
|
.css('display', multiple_values ? 'inline-block' : 'none')
|
@@ -118,6 +119,13 @@
|
|
118
119
|
.prop('name', operator_name)
|
119
120
|
.append('<option value="_discard">...</option>')
|
120
121
|
.append($('<option data-additional-fieldset="additional-fieldset" value="like"></option>').prop('selected', field_operator == "like").text(RailsAdmin.I18n.t("contains")))
|
122
|
+
.append(
|
123
|
+
$(
|
124
|
+
'<option data-additional-fieldset="additional-fieldset" value="not_like"></option>'
|
125
|
+
)
|
126
|
+
.prop("selected", field_operator == "not_like")
|
127
|
+
.text(RailsAdmin.I18n.t("does_not_contain"))
|
128
|
+
)
|
121
129
|
.append($('<option data-additional-fieldset="additional-fieldset" value="is"></option>').prop('selected', field_operator == "is").text(RailsAdmin.I18n.t("is_exactly")))
|
122
130
|
.append($('<option data-additional-fieldset="additional-fieldset" value="starts_with"></option>').prop('selected', field_operator == "starts_with").text(RailsAdmin.I18n.t("starts_with")))
|
123
131
|
.append($('<option data-additional-fieldset="additional-fieldset" value="ends_with"></option>').prop('selected', field_operator == "ends_with").text(RailsAdmin.I18n.t("ends_with")))
|
@@ -103,7 +103,7 @@
|
|
103
103
|
%td.other.left= link_to "...", @other_left_link, class: 'pjax'
|
104
104
|
- properties.map{ |property| property.bind(:object, object) }.each do |property|
|
105
105
|
- value = property.pretty_value
|
106
|
-
%td{class: "#{property.css_class} #{property.type_css_class}", title:
|
106
|
+
%td{class: "#{property.css_class} #{property.type_css_class}", title: value}= value
|
107
107
|
- if @other_right_link ||= other_right && index_path(params.merge(set: (params[:set].to_i + 1)))
|
108
108
|
%td.other.right= link_to "...", @other_right_link, class: 'pjax'
|
109
109
|
- unless frozen_columns
|
@@ -250,7 +250,7 @@ module RailsAdmin
|
|
250
250
|
|
251
251
|
@value = begin
|
252
252
|
case @operator
|
253
|
-
when 'default', 'like'
|
253
|
+
when 'default', 'like', 'not_like'
|
254
254
|
"%#{@value}%"
|
255
255
|
when 'starts_with'
|
256
256
|
"#{@value}%"
|
@@ -262,7 +262,13 @@ module RailsAdmin
|
|
262
262
|
end
|
263
263
|
|
264
264
|
if ['postgresql', 'postgis'].include? ar_adapter
|
265
|
-
|
265
|
+
if @operator == 'not_like'
|
266
|
+
["(#{@column} NOT ILIKE ?)", @value]
|
267
|
+
else
|
268
|
+
["(#{@column} ILIKE ?)", @value]
|
269
|
+
end
|
270
|
+
elsif @operator == 'not_like'
|
271
|
+
["(LOWER(#{@column}) NOT LIKE ?)", @value]
|
266
272
|
else
|
267
273
|
["(LOWER(#{@column}) LIKE ?)", @value]
|
268
274
|
end
|
@@ -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
|
@@ -258,6 +258,8 @@ module RailsAdmin
|
|
258
258
|
return if @value.blank?
|
259
259
|
@value = begin
|
260
260
|
case @operator
|
261
|
+
when 'not_like'
|
262
|
+
Regexp.compile("^((?!#{Regexp.escape(@value)}).)*$", Regexp::IGNORECASE)
|
261
263
|
when 'default', 'like'
|
262
264
|
Regexp.compile(Regexp.escape(@value), Regexp::IGNORECASE)
|
263
265
|
when 'starts_with'
|
data/lib/rails_admin/config.rb
CHANGED
@@ -198,7 +198,7 @@ module RailsAdmin
|
|
198
198
|
end
|
199
199
|
|
200
200
|
def default_search_operator=(operator)
|
201
|
-
if %w(default like starts_with ends_with is =).include? operator
|
201
|
+
if %w(default like not_like starts_with ends_with is =).include? operator
|
202
202
|
@default_search_operator = operator
|
203
203
|
else
|
204
204
|
raise(ArgumentError.new("Search operator '#{operator}' not supported"))
|
data/lib/rails_admin/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_admin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Erik Michaels-Ober
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
- Petteri Kaapa
|
10
10
|
- Benoit Benezech
|
11
11
|
- Mitsuhiro Shibuya
|
12
|
-
autorequire:
|
12
|
+
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date:
|
15
|
+
date: 2024-07-06 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: builder
|
@@ -348,6 +348,7 @@ files:
|
|
348
348
|
- lib/rails_admin.rb
|
349
349
|
- lib/rails_admin/abstract_model.rb
|
350
350
|
- lib/rails_admin/adapters/active_record.rb
|
351
|
+
- lib/rails_admin/adapters/active_record.rb.bak
|
351
352
|
- lib/rails_admin/adapters/active_record/abstract_object.rb
|
352
353
|
- lib/rails_admin/adapters/active_record/association.rb
|
353
354
|
- lib/rails_admin/adapters/active_record/property.rb
|
@@ -599,7 +600,7 @@ homepage: https://github.com/sferik/rails_admin
|
|
599
600
|
licenses:
|
600
601
|
- MIT
|
601
602
|
metadata: {}
|
602
|
-
post_install_message:
|
603
|
+
post_install_message:
|
603
604
|
rdoc_options: []
|
604
605
|
require_paths:
|
605
606
|
- lib
|
@@ -614,8 +615,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
614
615
|
- !ruby/object:Gem::Version
|
615
616
|
version: 1.8.11
|
616
617
|
requirements: []
|
617
|
-
rubygems_version: 3.
|
618
|
-
signing_key:
|
618
|
+
rubygems_version: 3.2.33
|
619
|
+
signing_key:
|
619
620
|
specification_version: 4
|
620
621
|
summary: Admin for Rails
|
621
622
|
test_files: []
|