rails_admin_next 1.2.0 → 1.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/README.md +8 -4
- data/app/assets/stylesheets/rails_admin/skin.css +4 -0
- data/app/controllers/rails_admin_next/main_controller.rb +64 -7
- data/app/helpers/rails_admin_next/form_builder.rb +23 -4
- data/app/helpers/rails_admin_next/main_helper.rb +12 -1
- data/app/views/layouts/rails_admin_next/_head.html.erb +1 -1
- data/app/views/layouts/rails_admin_next/_navigation.html.erb +1 -1
- data/app/views/rails_admin_next/main/_form_nested_many.html.erb +9 -4
- data/app/views/rails_admin_next/main/_form_nested_one.html.erb +8 -3
- data/app/views/rails_admin_next/main/bulk_delete.html.erb +1 -1
- data/app/views/rails_admin_next/main/dashboard.html.erb +52 -50
- data/app/views/rails_admin_next/main/index.html.erb +3 -2
- data/docs/UPGRADING.md +160 -2
- data/docs/security.md +5 -0
- data/docs/upstream-parity.md +47 -3
- data/lib/rails_admin_next/abstract_model.rb +12 -4
- data/lib/rails_admin_next/adapters/active_record.rb +47 -3
- data/lib/rails_admin_next/config/actions/bulk_delete.rb +4 -4
- data/lib/rails_admin_next/config/actions/export.rb +2 -1
- data/lib/rails_admin_next/config/actions/index.rb +5 -21
- data/lib/rails_admin_next/config/actions/show.rb +1 -1
- data/lib/rails_admin_next/config/actions.rb +5 -12
- data/lib/rails_admin_next/config/fields/association.rb +1 -1
- data/lib/rails_admin_next/config/fields/types/enum.rb +8 -2
- data/lib/rails_admin_next/config/fields/types/serialized.rb +17 -2
- data/lib/rails_admin_next/config/fields/types/timestamp.rb +1 -0
- data/lib/rails_admin_next/config/has_fields.rb +1 -1
- data/lib/rails_admin_next/config.rb +4 -4
- data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +1 -1
- data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +4 -2
- data/lib/rails_admin_next/support/composite_keys_serializer.rb +8 -2
- data/lib/rails_admin_next/support/csv_converter.rb +45 -5
- data/lib/rails_admin_next/support/serialization_schema.rb +74 -0
- data/lib/rails_admin_next/version.rb +1 -1
- data/lib/rails_admin_next.rb +5 -16
- data/src/rails_admin/controllers/nested_form_controller.js +1 -1
- metadata +3 -2
|
@@ -249,7 +249,7 @@ module RailsAdminNext
|
|
|
249
249
|
|
|
250
250
|
# pool of all found model names from the whole application
|
|
251
251
|
def models_pool
|
|
252
|
-
(viable_models - excluded_models.collect(
|
|
252
|
+
(viable_models - excluded_models.collect { it.to_s.delete_prefix("::") }).uniq.sort
|
|
253
253
|
end
|
|
254
254
|
|
|
255
255
|
# Loads a model configuration instance from the registry or registers
|
|
@@ -272,7 +272,7 @@ module RailsAdminNext
|
|
|
272
272
|
when Class, ConstLoadSuppressor::ConstProxy
|
|
273
273
|
entity.name.to_sym
|
|
274
274
|
when String, Symbol
|
|
275
|
-
entity.to_sym
|
|
275
|
+
entity.to_s.delete_prefix("::").to_sym
|
|
276
276
|
else
|
|
277
277
|
entity.class.name.to_sym
|
|
278
278
|
end
|
|
@@ -369,7 +369,7 @@ module RailsAdminNext
|
|
|
369
369
|
#
|
|
370
370
|
# @see RailsAdminNext::Config.registry
|
|
371
371
|
def reset_model(model)
|
|
372
|
-
key = model.is_a?(Class) ? model.name.to_sym : model.to_sym
|
|
372
|
+
key = model.is_a?(Class) ? model.name.to_sym : model.to_s.delete_prefix("::").to_sym
|
|
373
373
|
@registry.delete(key)
|
|
374
374
|
end
|
|
375
375
|
|
|
@@ -395,7 +395,7 @@ module RailsAdminNext
|
|
|
395
395
|
private
|
|
396
396
|
|
|
397
397
|
def viable_models
|
|
398
|
-
included_models.collect(
|
|
398
|
+
included_models.collect { it.to_s.delete_prefix("::") }.presence || begin
|
|
399
399
|
@@system_models ||= # memoization for tests
|
|
400
400
|
([Rails.application] + Rails::Engine.subclasses.collect(&:instance)).flat_map do |app|
|
|
401
401
|
(app.paths["app/models"].to_a + app.config.eager_load_paths).collect do |load_path|
|
|
@@ -143,7 +143,7 @@ module RailsAdminNext
|
|
|
143
143
|
|
|
144
144
|
versions = object.nil? ? versions_for_model(model) : object.public_send(model.model.versions_association_name)
|
|
145
145
|
versions = versions.where("event LIKE ?", "%#{query}%") if query.present?
|
|
146
|
-
versions = versions.
|
|
146
|
+
versions = versions.reorder(sort)
|
|
147
147
|
|
|
148
148
|
paginate_versions(versions, all, (page.presence || 1).to_i, per_page.to_i)
|
|
149
149
|
end
|
|
@@ -40,11 +40,13 @@ module RailsAdminNext
|
|
|
40
40
|
|
|
41
41
|
# This is called when needing to scope a database query. It is called within the list
|
|
42
42
|
# and bulk_delete/destroy actions and should return a scope which limits the records
|
|
43
|
-
# to those which the user can perform the given action on.
|
|
43
|
+
# to those which the user can perform the given action on. A model with no policy
|
|
44
|
+
# Scope gets no records: bulk_delete deletes whatever this returns without a
|
|
45
|
+
# per-record check, so a forgotten Scope must not mean "everything".
|
|
44
46
|
def query(_action, abstract_model)
|
|
45
47
|
@controller.send(:policy_scope, abstract_model.model.all)
|
|
46
48
|
rescue ::Pundit::NotDefinedError
|
|
47
|
-
abstract_model.model.
|
|
49
|
+
abstract_model.model.none
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
# This is called in the new/create actions to determine the initial attributes for new
|
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
module RailsAdminNext
|
|
4
4
|
module Support
|
|
5
5
|
module CompositeKeysSerializer
|
|
6
|
+
# "_" joins the keys, so it is escaped as "~" plus its hex byte. "~" is escaped too so the
|
|
7
|
+
# escape cannot be forged, and both are URL-unreserved. Every other value serializes as-is,
|
|
8
|
+
# keeping existing links valid.
|
|
9
|
+
ESCAPED = /[_~]/
|
|
10
|
+
ESCAPE = /~(5F|7E)/
|
|
11
|
+
|
|
6
12
|
def self.serialize(keys)
|
|
7
|
-
keys.map { |key| key&.to_s&.gsub("
|
|
13
|
+
keys.map { |key| key&.to_s&.gsub(ESCAPED) { format("~%02X", it.ord) } }.join("_")
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
def self.deserialize(string)
|
|
11
|
-
string.split("_").map { |key| key
|
|
17
|
+
string.split("_").map { |key| key.gsub(ESCAPE) { $1.hex.chr } }
|
|
12
18
|
end
|
|
13
19
|
end
|
|
14
20
|
end
|
|
@@ -4,14 +4,17 @@ require "csv"
|
|
|
4
4
|
|
|
5
5
|
module RailsAdminNext
|
|
6
6
|
class CSVConverter
|
|
7
|
+
BATCH_SIZE = 1000
|
|
8
|
+
|
|
7
9
|
def initialize(objects = [], schema = nil)
|
|
8
10
|
@fields = []
|
|
9
11
|
@associations = []
|
|
10
12
|
schema ||= {}
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
@objects = objects
|
|
15
|
+
@model = objects.is_a?(ActiveRecord::Relation) ? objects.klass : objects.first&.class
|
|
16
|
+
return unless @model
|
|
13
17
|
|
|
14
|
-
@model = objects.dup.first.class
|
|
15
18
|
@abstract_model = RailsAdminNext::AbstractModel.new(@model)
|
|
16
19
|
@model_config = @abstract_model.config
|
|
17
20
|
@methods = [(schema[:only] || []) + (schema[:methods] || [])].flatten.compact
|
|
@@ -63,17 +66,54 @@ module RailsAdminNext
|
|
|
63
66
|
|
|
64
67
|
def generate_csv_string(options)
|
|
65
68
|
generator_options = (options[:generator] || {}).symbolize_keys.delete_if { |_, value| value.blank? }
|
|
66
|
-
method = @objects.respond_to?(:find_each) ? :find_each : :each
|
|
67
|
-
|
|
68
69
|
CSV.generate(**generator_options) do |csv|
|
|
69
70
|
csv << generate_csv_header unless options[:skip_header]
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
each_object do |object|
|
|
72
73
|
csv << generate_csv_row(object)
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
def each_object(&)
|
|
79
|
+
return @objects.each(&) unless @objects.is_a?(ActiveRecord::Relation)
|
|
80
|
+
# Batching sets its own limit and offset, which would drop the relation's.
|
|
81
|
+
return @objects.each(&) if @objects.limit_value || @objects.offset_value
|
|
82
|
+
|
|
83
|
+
if (direction = primary_key_direction)
|
|
84
|
+
@objects.unscope(:order).find_each(order: direction, batch_size: BATCH_SIZE, &)
|
|
85
|
+
else
|
|
86
|
+
each_in_offset_batches(&)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# sort_scope writes the list sort as a single "<table>.<column> <direction>" string.
|
|
91
|
+
def primary_key_direction
|
|
92
|
+
primary_key = @model.primary_key
|
|
93
|
+
return unless primary_key.is_a?(String)
|
|
94
|
+
|
|
95
|
+
orders = @objects.order_values
|
|
96
|
+
return :asc if orders.empty?
|
|
97
|
+
return unless orders.one? && orders.first.is_a?(String)
|
|
98
|
+
|
|
99
|
+
table = Regexp.escape(@model.table_name)
|
|
100
|
+
column = Regexp.escape(primary_key)
|
|
101
|
+
orders.first[/\A(?:\W?#{table}\W?\.)?\W?#{column}\W? (asc|desc)\z/i, 1]&.downcase&.to_sym
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The primary-key tie-break makes the order total, so rows that tie on the sort
|
|
105
|
+
# column cannot shift between batches. A short batch is not the last one: sorting
|
|
106
|
+
# on an eager-loaded has-many column returns fewer records than the limit.
|
|
107
|
+
def each_in_offset_batches(&)
|
|
108
|
+
ordered = @objects.order(Array(@model.primary_key).index_with(:asc))
|
|
109
|
+
0.step(by: BATCH_SIZE) do |offset|
|
|
110
|
+
batch = ordered.limit(BATCH_SIZE).offset(offset).to_a
|
|
111
|
+
break if batch.empty?
|
|
112
|
+
|
|
113
|
+
batch.each(&)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
77
117
|
def generate_csv_header
|
|
78
118
|
@fields.collect do |field|
|
|
79
119
|
::I18n.t("admin.export.csv.header_for_root_methods", name: field.label, model: @abstract_model.pretty_name)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_admin_next/support/hash_helper"
|
|
4
|
+
|
|
5
|
+
module RailsAdminNext
|
|
6
|
+
# The options handed to to_json/serializable_hash, limited to what a section
|
|
7
|
+
# shows. Both serializers call every name under `methods` and `include` on each
|
|
8
|
+
# record, and serialize every attribute when `only` is missing, so a requested
|
|
9
|
+
# schema is only ever intersected with this, never passed through.
|
|
10
|
+
class SerializationSchema
|
|
11
|
+
def initialize(section, include_associations:)
|
|
12
|
+
@section = section
|
|
13
|
+
@include_associations = include_associations
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Attributes only: a virtual field need not be a method on the record, so
|
|
17
|
+
# calling it unasked could raise where the plain serialization never did.
|
|
18
|
+
def default
|
|
19
|
+
{only: allowed[:only]}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def sanitize(requested)
|
|
23
|
+
requested = HashHelper.symbolize(requested)
|
|
24
|
+
schema = filter(requested, allowed)
|
|
25
|
+
if @include_associations && requested[:include].is_a?(Hash)
|
|
26
|
+
schema[:include] = requested[:include].each_with_object({}) do |(name, value), included|
|
|
27
|
+
next unless value.is_a?(Hash) && allowed[:include].key?(name)
|
|
28
|
+
|
|
29
|
+
included[name] = filter(value, allowed[:include][name])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
schema
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def allowed
|
|
38
|
+
@allowed ||= begin
|
|
39
|
+
fields = @section.visible_fields
|
|
40
|
+
names = field_names(fields.select { |f| !f.association? || f.association.polymorphic? })
|
|
41
|
+
if @include_associations
|
|
42
|
+
names[:include] = fields.select { |f| f.association? && !f.association.polymorphic? }.to_h do |field|
|
|
43
|
+
associated = field.associated_model_config.export.with(@section.bindings.merge(object: field.associated_model_config.abstract_model.model.new))
|
|
44
|
+
[field.name, field_names(associated.visible_fields.reject(&:association?))]
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
# The section already shows these associations, so their keys expose nothing more
|
|
48
|
+
names[:only].concat(fields.select { |f| f.association? && f.association.type == :belongs_to && !f.association.polymorphic? }
|
|
49
|
+
.flat_map { |f| Array(f.association.foreign_key).map(&:to_sym) })
|
|
50
|
+
end
|
|
51
|
+
names
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Mirrors the names the export form offers (main/export.html.erb).
|
|
56
|
+
def field_names(fields)
|
|
57
|
+
fields.each_with_object({only: [], methods: []}) do |field, names|
|
|
58
|
+
list = field.virtual? ? names[:methods] : names[:only]
|
|
59
|
+
if field.association?
|
|
60
|
+
list << field.method_name.to_sym << field.association.foreign_type.to_sym
|
|
61
|
+
else
|
|
62
|
+
list << field.name.to_sym
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# An empty `only` serializes no attribute in ActiveRecord, which is the point.
|
|
68
|
+
def filter(requested, permitted)
|
|
69
|
+
schema = {only: Array(requested[:only]) & permitted[:only]}
|
|
70
|
+
schema[:methods] = Array(requested[:methods]) & permitted[:methods] if requested.key?(:methods)
|
|
71
|
+
schema
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/rails_admin_next.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "rails_admin_next/extensions/paper_trail"
|
|
|
12
12
|
require "rails_admin_next/support/csv_converter"
|
|
13
13
|
require "rails_admin_next/support/hash_helper"
|
|
14
14
|
require "rails_admin_next/support/paginated_collection"
|
|
15
|
+
require "rails_admin_next/support/serialization_schema"
|
|
15
16
|
require "yaml"
|
|
16
17
|
|
|
17
18
|
module RailsAdminNext
|
|
@@ -48,25 +49,13 @@ module RailsAdminNext
|
|
|
48
49
|
@deprecator ||= ActiveSupport::Deprecation.new(RailsAdminNext::Version.to_s, "RailsAdminNext")
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
begin
|
|
55
|
-
require "safe_yaml/load"
|
|
56
|
-
def self.yaml_load(yaml)
|
|
57
|
-
SafeYAML.load(yaml)
|
|
58
|
-
end
|
|
59
|
-
rescue LoadError
|
|
60
|
-
if YAML.respond_to?(:safe_load)
|
|
61
|
-
def self.yaml_load(yaml)
|
|
62
|
-
YAML.safe_load(yaml)
|
|
63
|
-
end
|
|
64
|
-
else
|
|
65
|
-
raise LoadError.new "Safe-loading of YAML is not available. Please install 'safe_yaml' or install Psych 2.0+"
|
|
66
|
-
end
|
|
52
|
+
def self.yaml_load(yaml)
|
|
53
|
+
deprecator.warn("RailsAdminNext.yaml_load is deprecated and will be removed. Call YAML.safe_load directly.")
|
|
54
|
+
YAML.safe_load(yaml)
|
|
67
55
|
end
|
|
68
56
|
|
|
69
57
|
def self.yaml_dump(object)
|
|
58
|
+
deprecator.warn("RailsAdminNext.yaml_dump is deprecated and will be removed. Call YAML.dump directly.")
|
|
70
59
|
YAML.dump(object)
|
|
71
60
|
end
|
|
72
61
|
end
|
|
@@ -7,7 +7,7 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
7
7
|
//
|
|
8
8
|
// - add: clones a blank subform from the <template>. For a has_many
|
|
9
9
|
// association the template carries a child-index placeholder
|
|
10
|
-
// (e.g. "
|
|
10
|
+
// (e.g. "new_1_nested_field_tests") which is swapped for a unique value so
|
|
11
11
|
// each new row submits under its own index; has_one/belongs_to subforms
|
|
12
12
|
// have no placeholder. Focus then moves to the first visible field.
|
|
13
13
|
// - remove on an unsaved subform deletes it from the DOM.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_admin_next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel López Prat
|
|
@@ -298,6 +298,7 @@ files:
|
|
|
298
298
|
- lib/rails_admin_next/support/csv_converter.rb
|
|
299
299
|
- lib/rails_admin_next/support/hash_helper.rb
|
|
300
300
|
- lib/rails_admin_next/support/paginated_collection.rb
|
|
301
|
+
- lib/rails_admin_next/support/serialization_schema.rb
|
|
301
302
|
- lib/rails_admin_next/version.rb
|
|
302
303
|
- lib/tasks/rails_admin.rake
|
|
303
304
|
- skills/rails-admin-next/SKILL.md
|
|
@@ -338,7 +339,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
338
339
|
requirements:
|
|
339
340
|
- - ">="
|
|
340
341
|
- !ruby/object:Gem::Version
|
|
341
|
-
version: 4.0.
|
|
342
|
+
version: 4.0.9
|
|
342
343
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
343
344
|
requirements:
|
|
344
345
|
- - ">="
|