rails_admin_next 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41aa74e3b01539b9fc293ce040bdc60801e0d9708d22447276c88dad216d6a51
4
- data.tar.gz: fa571294625b43664ae466e5c62aae7fb6e88a0c55a487562392c1670ddb4a36
3
+ metadata.gz: a47ededbcad2a173ee6e0d77e581ef0e4e7649458a7bf0e659592726299c6ae2
4
+ data.tar.gz: bded3ecf250c69185107635e6877b4d9467f678fb12919b08146a3e67fdb7842
5
5
  SHA512:
6
- metadata.gz: d43b9c62e232c5a6e37fae047f4712af5021b8263fcdb22034d18dea4550bc10e7c01e8b379f39cf356279248ace2f08b22ffb3abccdc05e8975338b323c9a7c
7
- data.tar.gz: 432b7fb059543a084a73b99dacf2d817c83f500bc3cc7af6335fc8c96d6883b7250f95470e4b2d9906a2677087feb8be80179658d36cc297cee9f417f553f917
6
+ metadata.gz: 88379320dc12d64a6311ed451b82a685862b7c7c3ea4449c44fc391a6b1cdad8beed31623a7d4709cc8bef210779cdfe41593e24b7a6688fdf7dcbdd101a6bbb
7
+ data.tar.gz: 6baf5beb0080b57a8f1fe08ccd6407872b1bec576fb43a493f58fefc76d5883c04f74c3d66e674756684c224038f6235c6f7e54b97483aa9d073a45c7dde8294
data/Gemfile CHANGED
@@ -7,40 +7,39 @@ gem "rails"
7
7
  gem "turbo-rails"
8
8
 
9
9
  group :development, :test do
10
- gem "pry", ">= 0.9"
10
+ gem "pry"
11
11
  end
12
12
 
13
13
  group :test do
14
14
  gem "brakeman", require: false
15
- gem "bundler-audit", require: false
16
15
  gem "cancancan", "~> 3.0"
17
- gem "cuprite", "!= 0.15.1"
18
- gem "database_cleaner-active_record", ">= 2.0", require: false
19
- gem "factory_bot", ">= 4.2", "!= 6.4.5"
20
- gem "generator_spec", ">= 0.8"
21
- gem "image_processing", ">= 1.2"
16
+ gem "cuprite"
17
+ gem "database_cleaner-active_record", require: false
18
+ gem "factory_bot"
19
+ gem "generator_spec"
20
+ gem "image_processing"
22
21
  gem "pundit"
23
- gem "rspec-expectations", "!= 3.8.3"
24
- gem "rspec-rails", ">= 4.0.0.beta2"
22
+ gem "rspec-rails"
25
23
  gem "rspec-retry"
26
- gem "ruby-vips", ">= 2.1"
27
- gem "simplecov", ">= 0.9", require: false
28
- gem "simplecov-lcov", require: false
29
- gem "standard", ">= 1.35.1", require: false
30
-
31
- # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
32
- gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
24
+ gem "ruby-vips"
25
+ gem "standard", require: false
33
26
  gem "warden"
27
+ # Capybara's own default server is puma, which is not in this bundle; spec_helper selects
28
+ # webrick instead. It arrives as a transitive of ferrum, so declare what the suite runs on
29
+ # rather than borrowing someone else's dependency.
30
+ gem "webrick"
31
+
32
+ # Windows does not ship zoneinfo files.
33
+ gem "tzinfo-data", platforms: %i[windows]
34
34
  end
35
35
 
36
+ # The dummy app boots with `Bundler.require(*Rails.groups, :active_record)`, so this group is
37
+ # loaded in every environment — that is what puts the adapter on the load path outside :test.
36
38
  group :active_record do
37
- gem "paper_trail", ">= 12.0"
38
-
39
- platforms :ruby, :mswin, :mingw, :x64_mingw do
40
- gem "mysql2", ">= 0.3.14"
41
- gem "pg", ">= 1.0.0"
42
- gem "sqlite3", ">= 1.3.0"
43
- end
39
+ gem "mysql2"
40
+ gem "paper_trail"
41
+ gem "pg"
42
+ gem "sqlite3"
44
43
  end
45
44
 
46
45
  gemspec
@@ -108,10 +108,17 @@ module RailsAdminNext
108
108
  model_config.send(action).with(controller: self, view: view_context, object: @object).visible_fields
109
109
  end
110
110
 
111
- def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key])
111
+ def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key], nested_in = nil)
112
112
  return unless target_params.present?
113
113
 
114
- fields = visible_fields(action, model_config)
114
+ # A subform never draws the child's link back to the record being edited — the parent
115
+ # association names it, and FormBuilder#nested_field_association? skips it on that name.
116
+ # Deriving the allowlist from visible_fields alone would keep permitting keys no form of
117
+ # ours submits, so drop that one field here too. Only at nested depth: editing a Comment
118
+ # on its own is a legitimate way to set its commentable.
119
+ fields = visible_fields(action, model_config).reject do |field|
120
+ nested_in && field.is_a?(Config::Fields::Association) && field.name == nested_in.inverse_of
121
+ end
115
122
  allowed_methods = fields.collect(&:allowed_methods).flatten.uniq.collect(&:to_s) << "id" << "_destroy"
116
123
  fields.each { |field| field.parse_input(target_params) }
117
124
  target_params.slice!(*allowed_methods)
@@ -132,7 +139,7 @@ module RailsAdminNext
132
139
  []
133
140
  end
134
141
  children_params.each do |children_param|
135
- sanitize_params_for!(:nested, association.associated_model_config, children_param) if children_param.respond_to?(:values)
142
+ sanitize_params_for!(:nested, association.associated_model_config, children_param, association) if children_param.respond_to?(:values)
136
143
  end
137
144
  end
138
145
  end
@@ -164,10 +164,17 @@ module RailsAdminNext
164
164
 
165
165
  private
166
166
 
167
+ # A field the parent association names as its inverse is the child's link back to
168
+ # the record being edited, so the user is never asked to fill it. That name match
169
+ # stands on its own: a polymorphic child such as Comment#commentable declares no
170
+ # inverse_of, so gating it on the child's own inverse_of would never let it fire.
171
+ # It stays limited to association fields because a polymorphic `as:` is never checked
172
+ # against the child class, and a plain column could otherwise match it by name.
167
173
  def nested_field_association?(field, nested_in)
168
- field.inverse_of.presence && nested_in.presence && field.inverse_of == nested_in.name &&
169
- (@template.instance_variable_get(:@model_config).abstract_model == field.abstract_model ||
170
- field.name == nested_in.inverse_of)
174
+ nested_in.presence &&
175
+ ((field.is_a?(Config::Fields::Association) && field.name == nested_in.inverse_of) ||
176
+ (field.inverse_of.presence && field.inverse_of == nested_in.name &&
177
+ @template.instance_variable_get(:@model_config).abstract_model == field.abstract_model))
171
178
  end
172
179
  end
173
180
  end
@@ -10,17 +10,19 @@
10
10
  CSP out of the picture and lets :light/:dark pins override the OS preference. %>
11
11
  <meta content="<%= {auto: 'light dark', light: 'light', dark: 'dark'}.fetch(RailsAdminNext.config.color_scheme) %>" name="color-scheme">
12
12
  <%# The entry stylesheet pulls tokens/framework/skin via @import, which the browser only discovers
13
- after fetching and parsing the entry file. Preload the three targets so all four fetch in parallel. %>
14
- <link rel="preload" as="style" href="<%= asset_path('rails_admin/tokens.css') %>">
15
- <link rel="preload" as="style" href="<%= asset_path('rails_admin/framework.css') %>">
16
- <link rel="preload" as="style" href="<%= asset_path('rails_admin/skin.css') %>">
13
+ after fetching and parsing the entry file. Preload the three targets so all four fetch in parallel.
14
+ A nonce-based style-src blocks a preload the same as a stylesheet, so these carry the nonce too;
15
+ `tag` drops the attribute entirely when no policy is configured and the nonce is nil. %>
16
+ <% %w[tokens framework skin].each do |sheet| %>
17
+ <%= tag.link rel: "preload", as: "style", href: asset_path("rails_admin/#{sheet}.css"), nonce: content_security_policy_nonce %>
18
+ <% end %>
17
19
  <%# Date/time fields use native HTML5 inputs; the picker UI is the browser's. Icons are inline SVG (no icon font). %>
18
- <%= stylesheet_link_tag "rails_admin", media: :all, data: {'turbo-track': 'reload'} %>
20
+ <%= stylesheet_link_tag "rails_admin", media: :all, nonce: true, data: {'turbo-track': 'reload'} %>
19
21
  <%# Rich-text (ActionText) fields render a Trix editor; load its stylesheet so the toolbar is styled.
20
22
  trix.css ships with the action_text-trix gem and is on the Propshaft load path when ActionText is used.
21
23
  NOTE: trix.css is light-only for now — dark styling for the Trix toolbar/editor is deferred. %>
22
24
  <% if defined?(ActionText::Engine) %>
23
- <%= stylesheet_link_tag "trix", media: :all, data: {'turbo-track': 'reload'} %>
25
+ <%= stylesheet_link_tag "trix", media: :all, nonce: true, data: {'turbo-track': 'reload'} %>
24
26
  <% end %>
25
27
  <%# Zero-build delivery: browser-native ESM pinned by the engine-owned importmap, served by Propshaft.
26
28
  The importmap-rails helpers thread the request CSP nonce onto every inline <script> they emit. %>
@@ -53,6 +53,7 @@ en:
53
53
  flash:
54
54
  successful: "%{name} successfully %{action}"
55
55
  error: "%{name} failed to be %{action}"
56
+ delete_restricted: "%{name} failed to be deleted: %{reason}"
56
57
  noaction: "No actions were taken"
57
58
  model_not_found: "Model '%{model}' could not be found"
58
59
  object_not_found: "%{model} with id '%{id}' could not be found"
@@ -104,8 +104,31 @@ module RailsAdminNext
104
104
  options[:polymorphic] || false
105
105
  end
106
106
 
107
+ # A polymorphic `as:` names the belongs_to on the other side, and Rails resolves
108
+ # an inverse for conventional pairs that declare nothing — both are the
109
+ # association's inverse for form-nesting purposes. `inverse_of: false` opts out
110
+ # of all of it, the way it does in Rails.
111
+ #
112
+ # The declared option is read first because it has to be: asking Rails for the
113
+ # inverse of a polymorphic belongs_to that declares one raises, since there is no
114
+ # single class to look the name up on.
107
115
  def inverse_of
108
- options[:inverse_of].try :to_sym
116
+ return nil if options[:inverse_of] == false
117
+
118
+ options[:inverse_of].try(:to_sym) || association.inverse_of&.name || as
119
+ end
120
+
121
+ # Rails guards the presence validator it adds for a required belongs_to with
122
+ # an `if:` condition, and the validator scan in Fields::Base#required? skips
123
+ # conditional validators, so required-ness has to come from the reflection.
124
+ def required?
125
+ return false unless type == :belongs_to
126
+ # A `default:` association is filled by a before_validation callback, so the
127
+ # record validates without the user choosing anything.
128
+ return false if options[:default]
129
+
130
+ optional = options[:optional]
131
+ optional.nil? ? model.belongs_to_required_by_default : !optional
109
132
  end
110
133
 
111
134
  def read_only?
@@ -34,9 +34,14 @@ module RailsAdminNext
34
34
  unless params[:bulk_ids].blank?
35
35
  @objects = list_entries(@model_config, :destroy)
36
36
  unless @objects.blank?
37
- processed_objects = @abstract_model.destroy(@objects)
38
- destroyed = processed_objects.select(&:destroyed?)
39
- not_destroyed = processed_objects - destroyed
37
+ # Rescue per record: dependent: :restrict_with_exception raises, and an
38
+ # escaping raise would abandon every record behind it in the batch.
39
+ destroyed, not_destroyed = Array.wrap(@objects).partition do |object|
40
+ object.destroy
41
+ object.destroyed?
42
+ rescue ActiveRecord::DeleteRestrictionError
43
+ false
44
+ end
40
45
  destroyed.each do |object|
41
46
  @auditing_adapter&.delete_object(object, @abstract_model, _current_user)
42
47
  end
@@ -33,12 +33,20 @@ module RailsAdminNext
33
33
 
34
34
  elsif request.delete? # DESTROY
35
35
 
36
- @auditing_adapter&.delete_object(@object, @abstract_model, _current_user)
37
- if @object.destroy
38
- flash[:success] = t("admin.flash.successful", name: @model_config.label, action: t("admin.actions.delete.done"))
39
- redirect_to index_path
40
- else
41
- handle_save_error :delete
36
+ begin
37
+ if @object.destroy
38
+ @auditing_adapter&.delete_object(@object, @abstract_model, _current_user)
39
+ flash[:success] = t("admin.flash.successful", name: @model_config.label, action: t("admin.actions.delete.done"))
40
+ redirect_to index_path
41
+ else
42
+ handle_save_error :delete
43
+ end
44
+ rescue ActiveRecord::DeleteRestrictionError => e
45
+ # dependent: :restrict_with_exception raises instead of adding an error to the
46
+ # record, so there is nothing for handle_save_error to render — report the
47
+ # association the exception names and send the admin back where they came from.
48
+ flash[:error] = t("admin.flash.delete_restricted", name: @model_config.label, reason: e.message)
49
+ redirect_to back_or_index, status: :see_other
42
50
  end
43
51
 
44
52
  end
@@ -55,9 +55,13 @@ module RailsAdminNext
55
55
  option_name = option_name.to_s
56
56
  options[option_name] = nil
57
57
 
58
- # If it's a boolean create an alias for it and remove question mark
58
+ # If it's a boolean create an alias for it and remove question mark. The
59
+ # rebinding is what the rest of the method reads: the getter, the setter, the
60
+ # @<name>_registered variable and the recursion key all want the chopped name,
61
+ # while @config_options keeps the un-chopped one it was registered under.
59
62
  if option_name.end_with?("?")
60
- scope.send(:define_method, "#{option_name.chop!}?") do
63
+ option_name = option_name.chop
64
+ scope.send(:define_method, "#{option_name}?") do
61
65
  send(option_name)
62
66
  end
63
67
  end
@@ -215,7 +215,9 @@ module RailsAdminNext
215
215
  :nil
216
216
  end
217
217
 
218
- (@required ||= {})[context] ||= !!([name] + children_fields).uniq.detect do |column_name|
218
+ # An association Rails treats as optional can still be required by a validator on its
219
+ # foreign key, so the reflection answer widens the validator scan instead of replacing it.
220
+ (@required ||= {})[context] ||= (association? && properties.try(:required?)) || !!([name] + children_fields).uniq.detect do |column_name|
219
221
  abstract_model.model.validators_on(column_name).detect do |v|
220
222
  !(v.options[:allow_nil] || v.options[:allow_blank]) &&
221
223
  %i[presence numericality attachment_presence].include?(v.kind) &&
@@ -2,9 +2,12 @@
2
2
 
3
3
  module RailsAdminNext
4
4
  class Version
5
+ # Releases are cut by publishing a Release-Drafter draft: the gem-push workflow stamps
6
+ # the tag's version over these constants at build time, so the in-repo values are only
7
+ # the floor a local `gem build` produces.
5
8
  MAJOR = 1
6
9
  MINOR = 0
7
- PATCH = 0
10
+ PATCH = 2
8
11
  PRE = nil
9
12
 
10
13
  class << self
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.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel López Prat
@@ -340,7 +340,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
340
340
  - !ruby/object:Gem::Version
341
341
  version: 1.8.11
342
342
  requirements: []
343
- rubygems_version: 4.0.10
343
+ rubygems_version: 4.0.16
344
344
  specification_version: 4
345
345
  summary: Admin for Rails
346
346
  test_files: []