accepts_nested_attributes_for_public_id 1.0.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 +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/Rakefile +13 -0
- data/lib/accepts_nested_attributes_for_public_id/method_contracts.rb +205 -0
- data/lib/accepts_nested_attributes_for_public_id/method_patches.rb +261 -0
- data/lib/accepts_nested_attributes_for_public_id/version.rb +3 -0
- data/lib/accepts_nested_attributes_for_public_id.rb +4 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0c66a87c5663498998b2263ec28d9f1f1bafde4101e93190b1d3e336c2c12605
|
4
|
+
data.tar.gz: bed9e7a6048e6d2f9761acc39156f2bc359e2ba5bac144505061d592792faaa5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 45592a78a5438ef544dd687dbf52624efaf8e194d31a083736ace28dba20eb254670bb584e71d72bc2c141a078cbf7127d5909c8feb19f88d03feb85147b5868
|
7
|
+
data.tar.gz: a14e83ac5d39575dde70a0c0ec4ab5b0833ca681efc8b64a198d2417528cd03903c7f2818996f75cc56d1531c5011d00e26705600d63ecca1f092956a0512612
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Weston Ganger
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# AcceptsNestedAttributesForPublicId
|
2
|
+
|
3
|
+
<a href="https://badge.fury.io/rb/accepts_nested_attributes_for_public_id" target="_blank"><img height="21" style='border:0px;height:21px;' border='0' src="https://badge.fury.io/rb/accepts_nested_attributes_for_public_id.svg" alt="Gem Version"></a>
|
4
|
+
<a href='https://github.com/westonganger/accepts_nested_attributes_for_public_id/actions' target='_blank'><img src="https://github.com/westonganger/accepts_nested_attributes_for_public_id/workflows/Tests/badge.svg" style="max-width:100%;" height='21' style='border:0px;height:21px;' border='0' alt="CI Status"></a>
|
5
|
+
<a href='https://rubygems.org/gems/accepts_nested_attributes_for_public_id' target='_blank'><img height='21' style='border:0px;height:21px;' src='https://img.shields.io/gem/dt/accepts_nested_attributes_for_public_id?color=brightgreen&label=Rubygems%20Downloads' border='0' alt='RubyGems Downloads' /></a>
|
6
|
+
|
7
|
+
A patch for Rails to support using a public ID column instead of ID for use with `accepts_nested_attributes_for`
|
8
|
+
|
9
|
+
Supports Rails 5, 6, 7+
|
10
|
+
|
11
|
+
Why:
|
12
|
+
|
13
|
+
- By default ActiveRecord and `accepts_nested_attributes_for` does not respect `to_param` or provide any ability to utilize a public ID column. This results in your DB primary keys being exposed in your forms.
|
14
|
+
- This was [extracted from a PR to Rails core](https://github.com/rails/rails/pull/48390) until this functionality is otherwise achievable in Rails core proper.
|
15
|
+
|
16
|
+
# Installation
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'accepts_nested_attributes_for_public_id'
|
20
|
+
```
|
21
|
+
|
22
|
+
# Usage
|
23
|
+
|
24
|
+
You now have access to the following options:
|
25
|
+
|
26
|
+
### Option A: Define a accepts_nested_attributes_for_public_id_column method on your class
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class Post < ApplicationRecord
|
30
|
+
has_many :comments
|
31
|
+
accepts_nested_attributes_for :comments
|
32
|
+
end
|
33
|
+
|
34
|
+
class Comment < ApplicationRecord
|
35
|
+
belongs_to :post
|
36
|
+
|
37
|
+
def self.accepts_nested_attributes_for_public_id_column
|
38
|
+
:my_public_id_db_column
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
### Option B: Use the :public_id_column option on accepts_nested_attributes_for
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class Post < ApplicationRecord
|
47
|
+
has_many :comments
|
48
|
+
accepts_nested_attributes_for :comments, public_id_column: :my_public_id_db_column
|
49
|
+
end
|
50
|
+
|
51
|
+
class Comment < ApplicationRecord
|
52
|
+
belongs_to :post
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
# How is this safe
|
57
|
+
|
58
|
+
The code for Nested Attributes in Rails core has not changed since around Rails 4 (Rails 7.0 is the current release at the time of writing this)
|
59
|
+
|
60
|
+
Because this patch requires changes in the very middle of some larger sized methods we are unable to use `super` in the patches. This can make it fragile if new changes were introduced to Rails core.
|
61
|
+
|
62
|
+
We have taken steps to ensure that no issues are caused by any future Rails changes by adding [runtime contracts](./lib/accepts_nested_attributes_for_public_id/method_contracts.rb) that ensure the original method source matches our saved contract of the current sources of these methods. If a new Rails version were to change the original method source then you would receive a runtime error stating that we are unable to apply the patch until the gem has been made compatible with any changed code.
|
63
|
+
|
64
|
+
# Testing
|
65
|
+
|
66
|
+
```
|
67
|
+
RAILS_ENV=test bundle exec rake db:create
|
68
|
+
RAILS_ENV=test bundle exec rake db:migrate
|
69
|
+
bundle exec rspec
|
70
|
+
```
|
71
|
+
|
72
|
+
We can locally test different versions of Rails using `ENV['RAILS_VERSION']` and different database gems using `ENV['DB_GEM']`
|
73
|
+
|
74
|
+
```
|
75
|
+
export RAILS_VERSION=7.0
|
76
|
+
export DB_GEM=sqlite3
|
77
|
+
bundle install
|
78
|
+
bundle exec rspec
|
79
|
+
```
|
80
|
+
|
81
|
+
# Credits
|
82
|
+
|
83
|
+
Created & Maintained by [Weston Ganger](https://westonganger.com) - [@westonganger](https://github.com/westonganger)
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/lib/accepts_nested_attributes_for_public_id/version.rb')
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
### Allow to use dummy app rake/rails commands from gem base folder
|
5
|
+
APP_RAKEFILE = File.expand_path("spec/dummy_app/Rakefile", __dir__)
|
6
|
+
load 'rails/tasks/engine.rake'
|
7
|
+
|
8
|
+
require 'rspec/core/rake_task'
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
|
+
|
11
|
+
task test: [:spec]
|
12
|
+
|
13
|
+
task default: [:spec]
|
@@ -0,0 +1,205 @@
|
|
1
|
+
module AcceptsNestedAttributesForPublicId
|
2
|
+
def verify_method_contract!(method, contract)
|
3
|
+
if method.source.strip.gsub(/^\s*/, "") != contract.strip.gsub(/^\s*/, "")
|
4
|
+
raise RuntimeError.new("Method definition contract violated for '#{self.class.name}##{method.name}', cannot apply patch for accepts_nested_attribute_for_public_id")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
module_function :verify_method_contract!
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveSupport.on_load(:action_view) do
|
11
|
+
AcceptsNestedAttributesForPublicId.verify_method_contract!(
|
12
|
+
ActionView::Helpers::FormBuilder.instance_method(:fields_for_with_nested_attributes),
|
13
|
+
<<-'CODE'
|
14
|
+
def fields_for_with_nested_attributes(association_name, association, options, block)
|
15
|
+
name = "#{object_name}[#{association_name}_attributes]"
|
16
|
+
association = convert_to_model(association)
|
17
|
+
|
18
|
+
if association.respond_to?(:persisted?)
|
19
|
+
association = [association] if @object.public_send(association_name).respond_to?(:to_ary)
|
20
|
+
elsif !association.respond_to?(:to_ary)
|
21
|
+
association = @object.public_send(association_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
if association.respond_to?(:to_ary)
|
25
|
+
explicit_child_index = options[:child_index]
|
26
|
+
output = ActiveSupport::SafeBuffer.new
|
27
|
+
association.each do |child|
|
28
|
+
if explicit_child_index
|
29
|
+
options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
|
30
|
+
else
|
31
|
+
options[:child_index] = nested_child_index(name)
|
32
|
+
end
|
33
|
+
if content = fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
|
34
|
+
output << content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
output
|
38
|
+
elsif association
|
39
|
+
fields_for_nested_model(name, association, options, block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
CODE
|
43
|
+
)
|
44
|
+
|
45
|
+
AcceptsNestedAttributesForPublicId.verify_method_contract!(
|
46
|
+
ActionView::Helpers::FormBuilder.instance_method(:fields_for_nested_model),
|
47
|
+
<<-'CODE'
|
48
|
+
def fields_for_nested_model(name, object, fields_options, block)
|
49
|
+
object = convert_to_model(object)
|
50
|
+
emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
|
51
|
+
options.fetch(:include_id, true)
|
52
|
+
}
|
53
|
+
|
54
|
+
@template.fields_for(name, object, fields_options) do |f|
|
55
|
+
output = @template.capture(f, &block)
|
56
|
+
output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
|
57
|
+
output
|
58
|
+
end
|
59
|
+
end
|
60
|
+
CODE
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
ActiveSupport.on_load(:active_record) do
|
65
|
+
AcceptsNestedAttributesForPublicId.verify_method_contract!(
|
66
|
+
ActiveRecord::NestedAttributes::ClassMethods.instance_method(:accepts_nested_attributes_for),
|
67
|
+
<<-'CODE'
|
68
|
+
def accepts_nested_attributes_for(*attr_names)
|
69
|
+
options = { allow_destroy: false, update_only: false }
|
70
|
+
options.update(attr_names.extract_options!)
|
71
|
+
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
72
|
+
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
73
|
+
|
74
|
+
attr_names.each do |association_name|
|
75
|
+
if reflection = _reflect_on_association(association_name)
|
76
|
+
reflection.autosave = true
|
77
|
+
define_autosave_validation_callbacks(reflection)
|
78
|
+
|
79
|
+
nested_attributes_options = self.nested_attributes_options.dup
|
80
|
+
nested_attributes_options[association_name.to_sym] = options
|
81
|
+
self.nested_attributes_options = nested_attributes_options
|
82
|
+
|
83
|
+
type = (reflection.collection? ? :collection : :one_to_one)
|
84
|
+
generate_association_writer(association_name, type)
|
85
|
+
else
|
86
|
+
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
CODE
|
91
|
+
)
|
92
|
+
|
93
|
+
AcceptsNestedAttributesForPublicId.verify_method_contract!(
|
94
|
+
ActiveRecord::NestedAttributes.instance_method(:assign_nested_attributes_for_one_to_one_association),
|
95
|
+
<<-'CODE'
|
96
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
97
|
+
options = nested_attributes_options[association_name]
|
98
|
+
if attributes.respond_to?(:permitted?)
|
99
|
+
attributes = attributes.to_h
|
100
|
+
end
|
101
|
+
attributes = attributes.with_indifferent_access
|
102
|
+
existing_record = send(association_name)
|
103
|
+
|
104
|
+
if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
|
105
|
+
(options[:update_only] || existing_record.id.to_s == attributes["id"].to_s)
|
106
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
|
107
|
+
|
108
|
+
elsif attributes["id"].present?
|
109
|
+
raise_nested_attributes_record_not_found!(association_name, attributes["id"])
|
110
|
+
|
111
|
+
elsif !reject_new_record?(association_name, attributes)
|
112
|
+
assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
113
|
+
|
114
|
+
if existing_record && existing_record.new_record?
|
115
|
+
existing_record.assign_attributes(assignable_attributes)
|
116
|
+
association(association_name).initialize_attributes(existing_record)
|
117
|
+
else
|
118
|
+
method = :"build_#{association_name}"
|
119
|
+
if respond_to?(method)
|
120
|
+
send(method, assignable_attributes)
|
121
|
+
else
|
122
|
+
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
CODE
|
128
|
+
)
|
129
|
+
|
130
|
+
AcceptsNestedAttributesForPublicId.verify_method_contract!(
|
131
|
+
ActiveRecord::NestedAttributes.instance_method(:assign_nested_attributes_for_collection_association),
|
132
|
+
<<-'CODE'
|
133
|
+
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
134
|
+
options = nested_attributes_options[association_name]
|
135
|
+
if attributes_collection.respond_to?(:permitted?)
|
136
|
+
attributes_collection = attributes_collection.to_h
|
137
|
+
end
|
138
|
+
|
139
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
140
|
+
raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
141
|
+
end
|
142
|
+
|
143
|
+
check_record_limit!(options[:limit], attributes_collection)
|
144
|
+
|
145
|
+
if attributes_collection.is_a? Hash
|
146
|
+
keys = attributes_collection.keys
|
147
|
+
attributes_collection = if keys.include?("id") || keys.include?(:id)
|
148
|
+
[attributes_collection]
|
149
|
+
else
|
150
|
+
attributes_collection.values
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
association = association(association_name)
|
155
|
+
|
156
|
+
existing_records = if association.loaded?
|
157
|
+
association.target
|
158
|
+
else
|
159
|
+
attribute_ids = attributes_collection.filter_map { |a| a["id"] || a[:id] }
|
160
|
+
attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
|
161
|
+
end
|
162
|
+
|
163
|
+
attributes_collection.each do |attributes|
|
164
|
+
if attributes.respond_to?(:permitted?)
|
165
|
+
attributes = attributes.to_h
|
166
|
+
end
|
167
|
+
attributes = attributes.with_indifferent_access
|
168
|
+
|
169
|
+
if attributes["id"].blank?
|
170
|
+
unless reject_new_record?(association_name, attributes)
|
171
|
+
association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
|
172
|
+
end
|
173
|
+
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
|
174
|
+
unless call_reject_if(association_name, attributes)
|
175
|
+
# Make sure we are operating on the actual object which is in the association's
|
176
|
+
# proxy_target array (either by finding it, or adding it if not found)
|
177
|
+
# Take into account that the proxy_target may have changed due to callbacks
|
178
|
+
target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s }
|
179
|
+
if target_record
|
180
|
+
existing_record = target_record
|
181
|
+
else
|
182
|
+
association.add_to_target(existing_record, skip_callbacks: true)
|
183
|
+
end
|
184
|
+
|
185
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
186
|
+
end
|
187
|
+
else
|
188
|
+
raise_nested_attributes_record_not_found!(association_name, attributes["id"])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
CODE
|
193
|
+
)
|
194
|
+
|
195
|
+
AcceptsNestedAttributesForPublicId.verify_method_contract!(
|
196
|
+
ActiveRecord::NestedAttributes.instance_method(:raise_nested_attributes_record_not_found!),
|
197
|
+
<<-'CODE'
|
198
|
+
def raise_nested_attributes_record_not_found!(association_name, record_id)
|
199
|
+
model = self.class._reflect_on_association(association_name).klass.name
|
200
|
+
raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
|
201
|
+
model, "id", record_id)
|
202
|
+
end
|
203
|
+
CODE
|
204
|
+
)
|
205
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
ActiveSupport.on_load(:action_view) do
|
2
|
+
module ActionView
|
3
|
+
module Helpers
|
4
|
+
class FormBuilder
|
5
|
+
private
|
6
|
+
|
7
|
+
def fields_for_with_nested_attributes(association_name, association, options, block)
|
8
|
+
name = "#{object_name}[#{association_name}_attributes]"
|
9
|
+
association = convert_to_model(association)
|
10
|
+
|
11
|
+
if association.respond_to?(:persisted?)
|
12
|
+
association = [association] if @object.public_send(association_name).respond_to?(:to_ary)
|
13
|
+
elsif !association.respond_to?(:to_ary)
|
14
|
+
association = @object.public_send(association_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
### NEW
|
18
|
+
if @object.respond_to?(:nested_attributes_options)
|
19
|
+
options[:public_id_column] = @object.nested_attributes_options.dig(association_name.to_sym, :public_id_column)
|
20
|
+
end
|
21
|
+
### END NEW
|
22
|
+
|
23
|
+
if association.respond_to?(:to_ary)
|
24
|
+
explicit_child_index = options[:child_index]
|
25
|
+
output = ActiveSupport::SafeBuffer.new
|
26
|
+
association.each do |child|
|
27
|
+
if explicit_child_index
|
28
|
+
options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
|
29
|
+
else
|
30
|
+
options[:child_index] = nested_child_index(name)
|
31
|
+
end
|
32
|
+
if content = fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
|
33
|
+
output << content
|
34
|
+
end
|
35
|
+
end
|
36
|
+
output
|
37
|
+
elsif association
|
38
|
+
fields_for_nested_model(name, association, options, block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def fields_for_nested_model(name, object, fields_options, block)
|
43
|
+
object = convert_to_model(object)
|
44
|
+
emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
|
45
|
+
options.fetch(:include_id, true)
|
46
|
+
}
|
47
|
+
|
48
|
+
@template.fields_for(name, object, fields_options) do |f|
|
49
|
+
output = @template.capture(f, &block)
|
50
|
+
|
51
|
+
### ORIGINAL
|
52
|
+
# output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
|
53
|
+
### NEW
|
54
|
+
if f.object.class.respond_to?(:accepts_nested_attributes_for_public_id_column)
|
55
|
+
public_id_column = f.object.class.accepts_nested_attributes_for_public_id_column
|
56
|
+
public_id_value = f.object.send(public_id_column)
|
57
|
+
elsif fields_options[:public_id_column]
|
58
|
+
public_id_value = f.object.send(fields_options[:public_id_column])
|
59
|
+
fields_options.delete(:public_id_column)
|
60
|
+
else
|
61
|
+
public_id_value = f.object.id
|
62
|
+
end
|
63
|
+
output.concat f.hidden_field(:id, value: public_id_value) if output && emit_hidden_id && !f.emitted_hidden_id?
|
64
|
+
### END NEW
|
65
|
+
|
66
|
+
output
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
ActiveSupport.on_load(:active_record) do
|
75
|
+
module ActiveRecord
|
76
|
+
module NestedAttributes
|
77
|
+
module ClassMethods
|
78
|
+
def accepts_nested_attributes_for(*attr_names)
|
79
|
+
options = { allow_destroy: false, update_only: false }
|
80
|
+
options.update(attr_names.extract_options!)
|
81
|
+
### ORIGINAL
|
82
|
+
# options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
83
|
+
### NEW
|
84
|
+
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only, :public_id_column)
|
85
|
+
### END NEW
|
86
|
+
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
87
|
+
|
88
|
+
attr_names.each do |association_name|
|
89
|
+
if reflection = _reflect_on_association(association_name)
|
90
|
+
reflection.autosave = true
|
91
|
+
define_autosave_validation_callbacks(reflection)
|
92
|
+
|
93
|
+
nested_attributes_options = self.nested_attributes_options.dup
|
94
|
+
nested_attributes_options[association_name.to_sym] = options
|
95
|
+
self.nested_attributes_options = nested_attributes_options
|
96
|
+
|
97
|
+
type = (reflection.collection? ? :collection : :one_to_one)
|
98
|
+
generate_association_writer(association_name, type)
|
99
|
+
else
|
100
|
+
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
module NestedAttributes
|
108
|
+
private
|
109
|
+
|
110
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
111
|
+
options = nested_attributes_options[association_name]
|
112
|
+
if attributes.respond_to?(:permitted?)
|
113
|
+
attributes = attributes.to_h
|
114
|
+
end
|
115
|
+
attributes = attributes.with_indifferent_access
|
116
|
+
existing_record = send(association_name)
|
117
|
+
|
118
|
+
### ORIGINAL
|
119
|
+
# if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
|
120
|
+
# (options[:update_only] || existing_record.id.to_s == attributes["id"].to_s)
|
121
|
+
### NEW
|
122
|
+
if self.class.reflect_on_association(association_name).klass.respond_to?(:accepts_nested_attributes_for_public_id_column)
|
123
|
+
public_id_column = self.class.reflect_on_association(association_name).klass.accepts_nested_attributes_for_public_id_column
|
124
|
+
elsif options[:public_id_column]
|
125
|
+
public_id_column = options[:public_id_column]
|
126
|
+
else
|
127
|
+
public_id_column = :id
|
128
|
+
end
|
129
|
+
|
130
|
+
if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
|
131
|
+
(options[:update_only] || existing_record.send(public_id_column).to_s == attributes["id"].to_s)
|
132
|
+
### END NEW
|
133
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
|
134
|
+
|
135
|
+
elsif attributes["id"].present?
|
136
|
+
raise_nested_attributes_record_not_found!(association_name, attributes["id"])
|
137
|
+
|
138
|
+
elsif !reject_new_record?(association_name, attributes)
|
139
|
+
assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
140
|
+
|
141
|
+
if existing_record && existing_record.new_record?
|
142
|
+
existing_record.assign_attributes(assignable_attributes)
|
143
|
+
association(association_name).initialize_attributes(existing_record)
|
144
|
+
else
|
145
|
+
method = :"build_#{association_name}"
|
146
|
+
if respond_to?(method)
|
147
|
+
send(method, assignable_attributes)
|
148
|
+
else
|
149
|
+
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
156
|
+
options = nested_attributes_options[association_name]
|
157
|
+
if attributes_collection.respond_to?(:permitted?)
|
158
|
+
attributes_collection = attributes_collection.to_h
|
159
|
+
end
|
160
|
+
|
161
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
162
|
+
raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
163
|
+
end
|
164
|
+
|
165
|
+
check_record_limit!(options[:limit], attributes_collection)
|
166
|
+
|
167
|
+
if attributes_collection.is_a? Hash
|
168
|
+
keys = attributes_collection.keys
|
169
|
+
attributes_collection = if keys.include?("id") || keys.include?(:id)
|
170
|
+
[attributes_collection]
|
171
|
+
else
|
172
|
+
attributes_collection.values
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
association = association(association_name)
|
177
|
+
|
178
|
+
### NEW
|
179
|
+
if association.klass.respond_to?(:accepts_nested_attributes_for_public_id_column)
|
180
|
+
public_id_column = association.klass.accepts_nested_attributes_for_public_id_column
|
181
|
+
elsif nested_attributes_options[association.reflection.name][:public_id_column]
|
182
|
+
public_id_column = nested_attributes_options[association.reflection.name][:public_id_column]
|
183
|
+
else
|
184
|
+
public_id_column = :id
|
185
|
+
end
|
186
|
+
### END NEW
|
187
|
+
|
188
|
+
existing_records = if association.loaded?
|
189
|
+
association.target
|
190
|
+
else
|
191
|
+
attribute_ids = attributes_collection.filter_map { |a| a["id"] || a[:id] }
|
192
|
+
### ORIGINAL
|
193
|
+
# attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
|
194
|
+
### NEW
|
195
|
+
attribute_ids.empty? ? [] : association.scope.where(public_id_column => attribute_ids)
|
196
|
+
### END NEW
|
197
|
+
end
|
198
|
+
|
199
|
+
attributes_collection.each do |attributes|
|
200
|
+
if attributes.respond_to?(:permitted?)
|
201
|
+
attributes = attributes.to_h
|
202
|
+
end
|
203
|
+
attributes = attributes.with_indifferent_access
|
204
|
+
|
205
|
+
if attributes["id"].blank?
|
206
|
+
unless reject_new_record?(association_name, attributes)
|
207
|
+
association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
|
208
|
+
end
|
209
|
+
### ORIGINAL
|
210
|
+
# elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s }
|
211
|
+
### NEW
|
212
|
+
elsif existing_record = existing_records.detect { |record| record.send(public_id_column).to_s == attributes["id"].to_s }
|
213
|
+
### END NEW
|
214
|
+
unless call_reject_if(association_name, attributes)
|
215
|
+
# Make sure we are operating on the actual object which is in the association's
|
216
|
+
# proxy_target array (either by finding it, or adding it if not found)
|
217
|
+
# Take into account that the proxy_target may have changed due to callbacks
|
218
|
+
### ORIGINAL
|
219
|
+
# target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s }
|
220
|
+
### NEW
|
221
|
+
target_record = association.target.detect { |record| record.send(public_id_column).to_s == attributes["id"].to_s }
|
222
|
+
### END NEW
|
223
|
+
|
224
|
+
if target_record
|
225
|
+
existing_record = target_record
|
226
|
+
else
|
227
|
+
association.add_to_target(existing_record, skip_callbacks: true)
|
228
|
+
end
|
229
|
+
|
230
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
231
|
+
end
|
232
|
+
else
|
233
|
+
raise_nested_attributes_record_not_found!(association_name, attributes["id"])
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def raise_nested_attributes_record_not_found!(association_name, record_id)
|
239
|
+
model = self.class._reflect_on_association(association_name).klass.name
|
240
|
+
### ORIGINAL
|
241
|
+
# raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
|
242
|
+
# model, "id", record_id)
|
243
|
+
### NEW
|
244
|
+
if self.class._reflect_on_association(association_name).klass.respond_to?(:accepts_nested_attributes_for_public_id_column)
|
245
|
+
id_column = self.class._reflect_on_association(association_name).klass.accepts_nested_attributes_for_public_id_column
|
246
|
+
elsif nested_attributes_options[association_name][:public_id_column]
|
247
|
+
id_column = nested_attributes_options[association_name][:public_id_column].to_s
|
248
|
+
else
|
249
|
+
id_column = "id"
|
250
|
+
end
|
251
|
+
raise RecordNotFound.new(
|
252
|
+
"Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
|
253
|
+
model,
|
254
|
+
id_column,
|
255
|
+
record_id,
|
256
|
+
)
|
257
|
+
### END NEW
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
metadata
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: accepts_nested_attributes_for_public_id
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Weston Ganger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-08-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: actionview
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rails-controller-testing
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: warning
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry-rails
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: A patch for Rails to support using a public ID column instead of ID for
|
140
|
+
use with accepts_nested_attributes_for
|
141
|
+
email:
|
142
|
+
- weston@westonganger.com
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- CHANGELOG.md
|
148
|
+
- LICENSE
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- lib/accepts_nested_attributes_for_public_id.rb
|
152
|
+
- lib/accepts_nested_attributes_for_public_id/method_contracts.rb
|
153
|
+
- lib/accepts_nested_attributes_for_public_id/method_patches.rb
|
154
|
+
- lib/accepts_nested_attributes_for_public_id/version.rb
|
155
|
+
homepage: https://github.com/westonganger/accepts_nested_attributes_for_public_id
|
156
|
+
licenses:
|
157
|
+
- MIT
|
158
|
+
metadata:
|
159
|
+
source_code_uri: https://github.com/westonganger/accepts_nested_attributes_for_public_id
|
160
|
+
changelog_uri: https://github.com/westonganger/accepts_nested_attributes_for_public_id/blob/master/CHANGELOG.md
|
161
|
+
post_install_message:
|
162
|
+
rdoc_options: []
|
163
|
+
require_paths:
|
164
|
+
- lib
|
165
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - ">="
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
requirements: []
|
176
|
+
rubygems_version: 3.4.6
|
177
|
+
signing_key:
|
178
|
+
specification_version: 4
|
179
|
+
summary: A patch for Rails to support using a public ID column instead of ID for use
|
180
|
+
with accepts_nested_attributes_for
|
181
|
+
test_files: []
|