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 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
@@ -0,0 +1,8 @@
1
+ # CHANGELOG
2
+
3
+ - **Unreleased**
4
+ * [View Diff](https://github.com/westonganger/accepts_nested_attributes_for_public_id/compare/v1.0.0...master)
5
+ * Nothing yet
6
+
7
+ - **v1.0.0**
8
+ * Initial gem release
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
@@ -0,0 +1,3 @@
1
+ module AcceptsNestedAttributesForPublicId
2
+ VERSION = "1.0.0".freeze
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'active_support/lazy_load_hooks'
2
+ require "accepts_nested_attributes_for_public_id/version"
3
+ require "accepts_nested_attributes_for_public_id/method_contracts"
4
+ require "accepts_nested_attributes_for_public_id/method_patches"
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: []