nested_store_attributes 0.0.1
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/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +32 -0
- data/lib/nested_store_attributes.rb +3 -0
- data/lib/nested_store_attributes/accepts_store_attributes.rb +248 -0
- data/lib/nested_store_attributes/version.rb +3 -0
- data/lib/tasks/nested_json_attributes_tasks.rake +4 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/person.rb +9 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +78 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/assets.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20141016114846_create_people.rb +11 -0
- data/test/dummy/db/schema.rb +24 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +16 -0
- data/test/dummy/log/test.log +1184 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/test/fixtures/people.yml +11 -0
- data/test/dummy/test/models/person_test.rb +7 -0
- data/test/nested_json_attributes_test.rb +70 -0
- data/test/test_helper.rb +15 -0
- metadata +170 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: 77ec0ac6acc18a674c3943d7e3aff59c10e8f8b2
|
4
|
+
data.tar.gz: 23c9447ceca1d84c635e3dcc59d05a58c8387ba0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 24efa0ff30e400a89957e6fb79134fe6320da481c0ba0930226d059f85fa3f548015318889db5816881278dbd3411ea707ac6772260a9146d8bfb569c739e92d
|
7
|
+
data.tar.gz: c2395cbe5c420d0a905994578dbacdb137c5973ae885a5d4beb590f9ca243bc4cb63b0b6213abb885b2078a36398aeca480f3b4c593fb45ef04a112a3c565bec
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'NestedStoreAttributes'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'active_support/core_ext/hash/except'
|
2
|
+
require 'active_support/core_ext/object/try'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
|
5
|
+
module NestedStoreAttributes
|
6
|
+
module AcceptsStoreAttributes #:nodoc:
|
7
|
+
class TooManyRecords < ActiveRecord::ActiveRecordError
|
8
|
+
end
|
9
|
+
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
class_attribute :store_attributes_options, instance_writer: false
|
14
|
+
self.store_attributes_options = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
|
19
|
+
|
20
|
+
# Defines an attributes writer for the specified serialized attribute(s).
|
21
|
+
#
|
22
|
+
# Supported options:
|
23
|
+
# [:allow_destroy]
|
24
|
+
# If true, destroys any members from the attributes hash with a
|
25
|
+
# <tt>_destroy</tt> key and a value that evaluates to +true+
|
26
|
+
# (eg. 1, '1', true, or 'true'). This option is off by default.
|
27
|
+
# [:reject_if]
|
28
|
+
# Allows you to specify a Proc or a Symbol pointing to a method
|
29
|
+
# that checks whether a record should be built for a certain attribute
|
30
|
+
# hash. The hash is passed to the supplied Proc or the method
|
31
|
+
# and it should return either +true+ or +false+. When no :reject_if
|
32
|
+
# is specified, a record will be built for all attribute hashes that
|
33
|
+
# do not have a <tt>_destroy</tt> value that evaluates to true.
|
34
|
+
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
|
35
|
+
# that will reject a record where all the attributes are blank excluding
|
36
|
+
# any value for _destroy.
|
37
|
+
# [:limit]
|
38
|
+
# Allows you to specify the maximum number of the nested records that
|
39
|
+
# can be processed with the nested attributes. Limit also can be specified as a
|
40
|
+
# Proc or a Symbol pointing to a method that should return number. If the size of the
|
41
|
+
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
|
42
|
+
# exception is raised. If omitted, any number nested records can be processed.
|
43
|
+
# [:primary_key]
|
44
|
+
# Allows you to specify the primary key to use when checking for existing objects. This
|
45
|
+
# defaults to :id.
|
46
|
+
#
|
47
|
+
# Examples:
|
48
|
+
# # creates subscribers_attributes=
|
49
|
+
# accepts_store_attributes_for :subscribers, primary_key: :email
|
50
|
+
# # creates books_attributes=
|
51
|
+
# accepts_store_attributes_for :books, reject_if: proc { |attributes| attributes['name'].blank? }
|
52
|
+
# # creates books_attributes=
|
53
|
+
# accepts_store_attributes_for :books, reject_if: :all_blank
|
54
|
+
# # creates books_attributes= and posts_attributes=
|
55
|
+
# accepts_store_attributes_for :books, :posts, allow_destroy: true
|
56
|
+
def accepts_store_attributes_for(*attr_names)
|
57
|
+
options = { :allow_destroy => false, :update_only => false, :primary_key => :id }
|
58
|
+
options.update(attr_names.extract_options!)
|
59
|
+
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only, :primary_key)
|
60
|
+
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
61
|
+
|
62
|
+
attr_names.each do |attribute_name|
|
63
|
+
if self.attribute_names.include?(attribute_name.to_s)
|
64
|
+
|
65
|
+
store_attributes_options = self.store_attributes_options.dup
|
66
|
+
store_attributes_options[attribute_name] = options
|
67
|
+
self.store_attributes_options = store_attributes_options
|
68
|
+
|
69
|
+
store_generate_collection_writer(attribute_name)
|
70
|
+
else
|
71
|
+
raise ArgumentError, "No column found for name `#{attribute_name}'. Has it been added yet?"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Generates a writer method for this attribute. Serves as a point for
|
79
|
+
# accessing the hashes in the attribute. For example, this method
|
80
|
+
# could generate the following:
|
81
|
+
#
|
82
|
+
# def pirate_attributes=(attributes)
|
83
|
+
# store_assign_nested_attributes_for_collection_association(:pirate, attributes)
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# This redirects the attempts to write objects in an association through
|
87
|
+
# the helper methods defined below. Makes it seem like the nested
|
88
|
+
# associations are just regular associations.
|
89
|
+
def store_generate_collection_writer(attribute_name)
|
90
|
+
generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
|
91
|
+
if method_defined?(:#{attribute_name}_attributes=)
|
92
|
+
remove_method(:#{attribute_name}_attributes=)
|
93
|
+
end
|
94
|
+
def #{attribute_name}_attributes=(attributes)
|
95
|
+
store_assign_nested_attributes_for_collection(:#{attribute_name}, attributes)
|
96
|
+
end
|
97
|
+
eoruby
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def _destroy
|
102
|
+
nil?
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Attribute hash keys that should not be assigned as normal attributes.
|
108
|
+
# These hash keys are nested attributes implementation details.
|
109
|
+
UNASSIGNABLE_KEYS = %w( id _destroy )
|
110
|
+
|
111
|
+
|
112
|
+
# Assigns the given attributes to the collection attribute.
|
113
|
+
#
|
114
|
+
# Hashes with an primary_key (by default <tt>:id</tt>) value matching an existing nested record
|
115
|
+
# will update that record. Hashes without an primary_key value will build
|
116
|
+
# a new record for the association. Hashes with a matching primary_key
|
117
|
+
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
|
118
|
+
# matched record for destruction.
|
119
|
+
#
|
120
|
+
# For example:
|
121
|
+
#
|
122
|
+
# store_assign_nested_attributes_for_collection_association(:people, {
|
123
|
+
# '1' => { id: '1', name: 'Peter' },
|
124
|
+
# '2' => { name: 'John' },
|
125
|
+
# '3' => { id: '2', _destroy: true }
|
126
|
+
# })
|
127
|
+
#
|
128
|
+
# Will update the name of the Person with ID 1, add a new record for
|
129
|
+
# person with the name 'John', and remove the Person with ID 2.
|
130
|
+
#
|
131
|
+
# Also accepts an Array of attribute hashes:
|
132
|
+
#
|
133
|
+
# store_assign_nested_attributes_for_collection_association(:people, [
|
134
|
+
# { id: '1', name: 'Peter' },
|
135
|
+
# { name: 'John' },
|
136
|
+
# { id: '2', _destroy: true }
|
137
|
+
# ])
|
138
|
+
def store_assign_nested_attributes_for_collection(attribute_name, attributes_collection)
|
139
|
+
options = self.store_attributes_options[attribute_name]
|
140
|
+
|
141
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
142
|
+
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
143
|
+
end
|
144
|
+
|
145
|
+
store_check_record_limit!(options[:limit], attributes_collection)
|
146
|
+
primary_key = options[:primary_key].to_s
|
147
|
+
new_collection = []
|
148
|
+
|
149
|
+
if attributes_collection.is_a? Hash
|
150
|
+
keys = attributes_collection.keys
|
151
|
+
attributes_collection = if keys.include?(primary_key) || keys.include?(primary_key.to_sym)
|
152
|
+
[attributes_collection]
|
153
|
+
else
|
154
|
+
attributes_collection.values
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
existing_records = self.send(attribute_name) || []
|
159
|
+
existing_records.map!(&:with_indifferent_access)
|
160
|
+
|
161
|
+
attributes_collection.each do |attributes|
|
162
|
+
attributes = attributes.with_indifferent_access
|
163
|
+
|
164
|
+
if attributes[primary_key].present? && existing_record = existing_records.delete_at(existing_records.index { |record| record[primary_key].to_s == attributes[primary_key].to_s } || existing_records.length)
|
165
|
+
unless store_call_reject_if(attribute_name, attributes)
|
166
|
+
new_collection << store_add_or_destroy(existing_record, attributes, options[:allow_destroy])
|
167
|
+
end
|
168
|
+
else
|
169
|
+
unless store_reject_new_record?(attribute_name, attributes)
|
170
|
+
new_collection << attributes.except(*UNASSIGNABLE_KEYS)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
new_collection += existing_records
|
176
|
+
new_collection.reject!(&:nil?)
|
177
|
+
self.assign_attributes(attribute_name => new_collection)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Takes in a limit and checks if the attributes_collection has too many
|
181
|
+
# records. It accepts limit in the form of symbol, proc, or
|
182
|
+
# number-like object (anything that can be compared with an integer).
|
183
|
+
#
|
184
|
+
# Raises TooManyRecords error if the attributes_collection is
|
185
|
+
# larger than the limit.
|
186
|
+
def store_check_record_limit!(limit, attributes_collection)
|
187
|
+
if limit
|
188
|
+
limit = case limit
|
189
|
+
when Symbol
|
190
|
+
send(limit)
|
191
|
+
when Proc
|
192
|
+
limit.call
|
193
|
+
else
|
194
|
+
limit
|
195
|
+
end
|
196
|
+
|
197
|
+
if limit && attributes_collection.size > limit
|
198
|
+
raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Updates a record with the +attributes+ or returns nil if
|
204
|
+
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
|
205
|
+
def store_add_or_destroy(hash, attributes, allow_destroy)
|
206
|
+
hash.merge!(attributes.except(*UNASSIGNABLE_KEYS))
|
207
|
+
unless store_has_destroy_flag?(attributes) && allow_destroy
|
208
|
+
return hash
|
209
|
+
else
|
210
|
+
return nil
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Determines if a hash contains a truthy _destroy key.
|
215
|
+
def store_has_destroy_flag?(hash)
|
216
|
+
hash.stringify_keys!
|
217
|
+
ActiveRecord::ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
|
218
|
+
end
|
219
|
+
|
220
|
+
# Determines if a new record should be rejected by checking
|
221
|
+
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
|
222
|
+
# attribute and evaluates to +true+.
|
223
|
+
def store_reject_new_record?(attribute_name, attributes)
|
224
|
+
store_has_destroy_flag?(attributes) || store_call_reject_if(attribute_name, attributes)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Determines if a record with the particular +attributes+ should be
|
228
|
+
# rejected by calling the reject_if Symbol or Proc (if defined).
|
229
|
+
# The reject_if option is defined by +accepts_nested_attributes_for+.
|
230
|
+
#
|
231
|
+
# Returns false if there is a +destroy_flag+ on the attributes.
|
232
|
+
def store_call_reject_if(attribute_name, attributes)
|
233
|
+
return false if store_has_destroy_flag?(attributes)
|
234
|
+
case callback = self.store_attributes_options[attribute_name][:reject_if]
|
235
|
+
when Symbol
|
236
|
+
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
237
|
+
when Proc
|
238
|
+
callback.call(attributes)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def store_raise_nested_attributes_record_not_found!(attribute_name, record_id)
|
243
|
+
raise RecordNotFound, "Couldn't find #{attribute_name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
ActiveRecord::Base.send :include, NestedStoreAttributes::AcceptsStoreAttributes
|
@@ -0,0 +1,28 @@
|
|
1
|
+
== README
|
2
|
+
|
3
|
+
This README would normally document whatever steps are necessary to get the
|
4
|
+
application up and running.
|
5
|
+
|
6
|
+
Things you may want to cover:
|
7
|
+
|
8
|
+
* Ruby version
|
9
|
+
|
10
|
+
* System dependencies
|
11
|
+
|
12
|
+
* Configuration
|
13
|
+
|
14
|
+
* Database creation
|
15
|
+
|
16
|
+
* Database initialization
|
17
|
+
|
18
|
+
* How to run the test suite
|
19
|
+
|
20
|
+
* Services (job queues, cache servers, search engines, etc.)
|
21
|
+
|
22
|
+
* Deployment instructions
|
23
|
+
|
24
|
+
* ...
|
25
|
+
|
26
|
+
|
27
|
+
Please feel free to use a different markup language if you do not plan to run
|
28
|
+
<tt>rake doc:app</tt>.
|
data/test/dummy/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any styles
|
10
|
+
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
11
|
+
* file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Dummy</title>
|
5
|
+
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
|
6
|
+
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|