accepts_nested_attributes_for_public_id 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []