dm-accepts_nested_attributes 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/CHANGELOG +970 -0
- data/LICENSE +20 -0
- data/README.textile +94 -0
- data/Rakefile +35 -0
- data/TODO +6 -0
- data/dm-accepts_nested_attributes.gemspec +90 -0
- data/lib/dm-accepts_nested_attributes.rb +7 -0
- data/lib/dm-accepts_nested_attributes/error_collecting.rb +35 -0
- data/lib/dm-accepts_nested_attributes/model.rb +155 -0
- data/lib/dm-accepts_nested_attributes/resource.rb +259 -0
- data/lib/dm-accepts_nested_attributes/transactional_save.rb +24 -0
- data/lib/dm-accepts_nested_attributes/version.rb +7 -0
- data/spec/accepts_nested_attributes_for_spec.rb +408 -0
- data/spec/many_to_many_spec.rb +129 -0
- data/spec/many_to_one_spec.rb +101 -0
- data/spec/one_to_many_spec.rb +100 -0
- data/spec/one_to_one_spec.rb +115 -0
- data/spec/rcov.opts +6 -0
- data/spec/shared/many_to_many_spec.rb +162 -0
- data/spec/shared/many_to_one_spec.rb +150 -0
- data/spec/shared/one_to_many_spec.rb +128 -0
- data/spec/shared/one_to_one_spec.rb +130 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +53 -0
- data/tasks/changelog.rake +20 -0
- data/tasks/ci.rake +1 -0
- data/tasks/metrics.rake +36 -0
- data/tasks/spec.rake +25 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +125 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Martin Gamsjäger
|
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.textile
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
h2. dm-accepts_nested_attributes
|
2
|
+
|
3
|
+
A DataMapper plugin that allows nested model attribute assignment like activerecord does.
|
4
|
+
|
5
|
+
Current documentation can always be found at "rdoc.info":http://rdoc.info/projects/snusnu/dm-accepts_nested_attributes
|
6
|
+
|
7
|
+
h3. Examples
|
8
|
+
|
9
|
+
The following example illustrates the use of this plugin.
|
10
|
+
|
11
|
+
<pre>
|
12
|
+
<code>
|
13
|
+
require "rubygems"
|
14
|
+
|
15
|
+
require "dm-core"
|
16
|
+
require "dm-validations"
|
17
|
+
require "dm-accepts_nested_attributes"
|
18
|
+
|
19
|
+
DataMapper::Logger.new(STDOUT, :debug)
|
20
|
+
DataMapper.setup(:default, 'sqlite3::memory:')
|
21
|
+
|
22
|
+
class Person
|
23
|
+
include DataMapper::Resource
|
24
|
+
property :id, Serial
|
25
|
+
property :name, String
|
26
|
+
has 1, :profile
|
27
|
+
has n, :project_memberships
|
28
|
+
has n, :projects, :through => :project_memberships
|
29
|
+
|
30
|
+
accepts_nested_attributes_for :profile
|
31
|
+
accepts_nested_attributes_for :projects
|
32
|
+
|
33
|
+
# adds the following instance methods
|
34
|
+
# #profile_attributes=
|
35
|
+
# #profile_attributes
|
36
|
+
# #projects_attributes=
|
37
|
+
# #projects_attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
class Profile
|
41
|
+
include DataMapper::Resource
|
42
|
+
property :id, Serial
|
43
|
+
property :person_id, Integer
|
44
|
+
belongs_to :person
|
45
|
+
|
46
|
+
accepts_nested_attributes_for :person
|
47
|
+
|
48
|
+
# adds the following instance methods
|
49
|
+
# #person_attributes=
|
50
|
+
# #person_attributes
|
51
|
+
end
|
52
|
+
|
53
|
+
class Project
|
54
|
+
include DataMapper::Resource
|
55
|
+
property :id, Serial
|
56
|
+
has n, :tasks
|
57
|
+
has n, :project_memberships
|
58
|
+
has n, :people, :through => :project_memberships
|
59
|
+
|
60
|
+
accepts_nested_attributes_for :tasks
|
61
|
+
accepts_nested_attributes_for :people
|
62
|
+
|
63
|
+
# adds the following instance methods
|
64
|
+
# #tasks_attributes=
|
65
|
+
# #tasks_attributes
|
66
|
+
# #people_attributes=
|
67
|
+
# #people_attributes
|
68
|
+
end
|
69
|
+
|
70
|
+
class ProjectMembership
|
71
|
+
include DataMapper::Resource
|
72
|
+
property :id, Serial
|
73
|
+
property :person_id, Integer
|
74
|
+
property :project_id, Integer
|
75
|
+
belongs_to :person
|
76
|
+
belongs_to :project
|
77
|
+
end
|
78
|
+
|
79
|
+
class Task
|
80
|
+
include DataMapper::Resource
|
81
|
+
property :id, Serial
|
82
|
+
property :project_id, Integer
|
83
|
+
belongs_to :project
|
84
|
+
end
|
85
|
+
|
86
|
+
DataMapper.auto_migrate!
|
87
|
+
</code>
|
88
|
+
</pre>
|
89
|
+
|
90
|
+
h2. TODO
|
91
|
+
|
92
|
+
* collect validation errors from related resources
|
93
|
+
* update README to include more complete usecases
|
94
|
+
* think about replacing :reject_if with :if and :unless
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require File.expand_path('../lib/dm-accepts_nested_attributes/version', __FILE__)
|
5
|
+
|
6
|
+
FileList['tasks/**/*.rake'].each { |task| load task }
|
7
|
+
|
8
|
+
begin
|
9
|
+
|
10
|
+
gem 'jeweler', '~> 1.4'
|
11
|
+
require 'jeweler'
|
12
|
+
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
|
15
|
+
gem.version = DataMapper::NestedAttributes::VERSION
|
16
|
+
|
17
|
+
gem.name = 'dm-accepts_nested_attributes'
|
18
|
+
gem.summary = 'Nested model assignment for datamapper'
|
19
|
+
gem.description = 'A datamapper plugin that allows nested model assignment like activerecord.'
|
20
|
+
gem.email = 'gamsnjaga [a] gmail [d] com'
|
21
|
+
gem.homepage = 'http://github.com/snusnu/dm-accepts_nested_attributes'
|
22
|
+
gem.authors = [ 'Martin Gamsjaeger' ]
|
23
|
+
|
24
|
+
gem.add_dependency 'dm-core', '~> 0.10.2'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rspec', '~> 1.2.9'
|
27
|
+
gem.add_development_dependency 'yard', '~> 0.4.0'
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
Jeweler::GemcutterTasks.new
|
32
|
+
|
33
|
+
rescue LoadError
|
34
|
+
puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler'
|
35
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-accepts_nested_attributes}
|
8
|
+
s.version = "0.12.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Martin Gamsjaeger"]
|
12
|
+
s.date = %q{2010-01-23}
|
13
|
+
s.description = %q{A datamapper plugin that allows nested model assignment like activerecord.}
|
14
|
+
s.email = %q{gamsnjaga [a] gmail [d] com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.textile",
|
18
|
+
"TODO"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"CHANGELOG",
|
23
|
+
"LICENSE",
|
24
|
+
"README.textile",
|
25
|
+
"Rakefile",
|
26
|
+
"TODO",
|
27
|
+
"dm-accepts_nested_attributes.gemspec",
|
28
|
+
"lib/dm-accepts_nested_attributes.rb",
|
29
|
+
"lib/dm-accepts_nested_attributes/error_collecting.rb",
|
30
|
+
"lib/dm-accepts_nested_attributes/model.rb",
|
31
|
+
"lib/dm-accepts_nested_attributes/resource.rb",
|
32
|
+
"lib/dm-accepts_nested_attributes/transactional_save.rb",
|
33
|
+
"lib/dm-accepts_nested_attributes/version.rb",
|
34
|
+
"spec/accepts_nested_attributes_for_spec.rb",
|
35
|
+
"spec/many_to_many_spec.rb",
|
36
|
+
"spec/many_to_one_spec.rb",
|
37
|
+
"spec/one_to_many_spec.rb",
|
38
|
+
"spec/one_to_one_spec.rb",
|
39
|
+
"spec/rcov.opts",
|
40
|
+
"spec/shared/many_to_many_spec.rb",
|
41
|
+
"spec/shared/many_to_one_spec.rb",
|
42
|
+
"spec/shared/one_to_many_spec.rb",
|
43
|
+
"spec/shared/one_to_one_spec.rb",
|
44
|
+
"spec/spec.opts",
|
45
|
+
"spec/spec_helper.rb",
|
46
|
+
"tasks/changelog.rake",
|
47
|
+
"tasks/ci.rake",
|
48
|
+
"tasks/metrics.rake",
|
49
|
+
"tasks/spec.rake",
|
50
|
+
"tasks/yard.rake",
|
51
|
+
"tasks/yardstick.rake"
|
52
|
+
]
|
53
|
+
s.homepage = %q{http://github.com/snusnu/dm-accepts_nested_attributes}
|
54
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
55
|
+
s.require_paths = ["lib"]
|
56
|
+
s.rubygems_version = %q{1.3.5}
|
57
|
+
s.summary = %q{Nested model assignment for datamapper}
|
58
|
+
s.test_files = [
|
59
|
+
"spec/accepts_nested_attributes_for_spec.rb",
|
60
|
+
"spec/many_to_many_spec.rb",
|
61
|
+
"spec/many_to_one_spec.rb",
|
62
|
+
"spec/one_to_many_spec.rb",
|
63
|
+
"spec/one_to_one_spec.rb",
|
64
|
+
"spec/shared/many_to_many_spec.rb",
|
65
|
+
"spec/shared/many_to_one_spec.rb",
|
66
|
+
"spec/shared/one_to_many_spec.rb",
|
67
|
+
"spec/shared/one_to_one_spec.rb",
|
68
|
+
"spec/spec_helper.rb"
|
69
|
+
]
|
70
|
+
|
71
|
+
if s.respond_to? :specification_version then
|
72
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
73
|
+
s.specification_version = 3
|
74
|
+
|
75
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
76
|
+
s.add_runtime_dependency(%q<dm-core>, ["~> 0.10.2"])
|
77
|
+
s.add_development_dependency(%q<rspec>, ["~> 1.2.9"])
|
78
|
+
s.add_development_dependency(%q<yard>, ["~> 0.4.0"])
|
79
|
+
else
|
80
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
81
|
+
s.add_dependency(%q<rspec>, ["~> 1.2.9"])
|
82
|
+
s.add_dependency(%q<yard>, ["~> 0.4.0"])
|
83
|
+
end
|
84
|
+
else
|
85
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
86
|
+
s.add_dependency(%q<rspec>, ["~> 1.2.9"])
|
87
|
+
s.add_dependency(%q<yard>, ["~> 0.4.0"])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
module ValidationErrorCollecting
|
5
|
+
|
6
|
+
# collect errors on parent associations
|
7
|
+
def before_save_parent_association(association, context)
|
8
|
+
if association.respond_to?(:each)
|
9
|
+
association.each do |r|
|
10
|
+
unless r.valid?(context)
|
11
|
+
r.errors.each { |e| self.errors.add(:general, e) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
else
|
15
|
+
unless association.valid?(context)
|
16
|
+
association.errors.each { |e| self.errors.add(:general, e) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# collect errors on child associations
|
22
|
+
def before_save_child_association(association, context)
|
23
|
+
if association.respond_to?(:valid?)
|
24
|
+
unless association.valid?(context)
|
25
|
+
association.errors.each { |e| self.errors.add(:general, e) }
|
26
|
+
end
|
27
|
+
else
|
28
|
+
self.errors.add(:general, "child association is missing")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
##
|
5
|
+
# Named plugin exception that gets raised by
|
6
|
+
# @see accepts_nested_attributes_for
|
7
|
+
# if the passed options don't make sense
|
8
|
+
class InvalidOptions < ArgumentError; end
|
9
|
+
|
10
|
+
module Model
|
11
|
+
|
12
|
+
##
|
13
|
+
# Allows any association to accept nested attributes.
|
14
|
+
#
|
15
|
+
# @param [Symbol, String] association_name
|
16
|
+
# The name of the association that should accept nested attributes
|
17
|
+
#
|
18
|
+
# @param [Hash, nil] options
|
19
|
+
# List of resources to initialize the Collection with
|
20
|
+
#
|
21
|
+
# @option [Symbol, String, #call] :reject_if
|
22
|
+
# An instance method name or an object that respond_to?(:call), which
|
23
|
+
# stops a new record from being created, if it evaluates to true.
|
24
|
+
#
|
25
|
+
# @option [true, false] :allow_destroy
|
26
|
+
# If true, allow destroying the association via the generated writer
|
27
|
+
# If false, prevent destroying the association via the generated writer
|
28
|
+
# defaults to false
|
29
|
+
#
|
30
|
+
# @raise [DataMapper::NestedAttributes::InvalidOptions]
|
31
|
+
# A named exception class indicating invalid options
|
32
|
+
#
|
33
|
+
# @return nil
|
34
|
+
#
|
35
|
+
def accepts_nested_attributes_for(association_name, options = {})
|
36
|
+
|
37
|
+
# ----------------------------------------------------------------------------------
|
38
|
+
# try to fail as early as possible
|
39
|
+
# ----------------------------------------------------------------------------------
|
40
|
+
|
41
|
+
unless relationship = relationships(repository_name)[association_name]
|
42
|
+
raise(ArgumentError, "No relationship #{association_name.inspect} for '#{name}' in :#{repository_name} repository")
|
43
|
+
end
|
44
|
+
|
45
|
+
# raise InvalidOptions if the given options don't make sense
|
46
|
+
assert_valid_options_for_nested_attributes(options)
|
47
|
+
|
48
|
+
# by default, nested attributes can't be destroyed
|
49
|
+
options = { :allow_destroy => false }.update(options)
|
50
|
+
|
51
|
+
# ----------------------------------------------------------------------------------
|
52
|
+
# should be safe to go from here
|
53
|
+
# ----------------------------------------------------------------------------------
|
54
|
+
|
55
|
+
options_for_nested_attributes[relationship] = options
|
56
|
+
|
57
|
+
include ::DataMapper::NestedAttributes::Resource
|
58
|
+
|
59
|
+
# TODO i wonder if this is the best place here?
|
60
|
+
# the transactional save behavior is definitely not needed for all resources,
|
61
|
+
# but it's necessary for resources that accept nested attributes
|
62
|
+
# FIXME this leads to weird "no such table" errors when specs are run
|
63
|
+
add_transactional_save_behavior # TODO if repository.adapter.supports_transactions?
|
64
|
+
|
65
|
+
# TODO make this do something
|
66
|
+
# it's only here now to remind me that this is probably the best place to put it
|
67
|
+
add_error_collection_behavior if DataMapper.const_defined?('Validate')
|
68
|
+
|
69
|
+
type = relationship.max > 1 ? :collection : :resource
|
70
|
+
|
71
|
+
define_method "#{association_name}_attributes" do
|
72
|
+
instance_variable_get("@#{association_name}_attributes")
|
73
|
+
end
|
74
|
+
|
75
|
+
define_method "#{association_name}_attributes=" do |attributes|
|
76
|
+
attributes = sanitize_nested_attributes(attributes)
|
77
|
+
instance_variable_set("@#{association_name}_attributes", attributes)
|
78
|
+
send("assign_nested_attributes_for_related_#{type}", relationship, attributes)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def options_for_nested_attributes
|
84
|
+
@options_for_nested_attributes ||= {}
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
##
|
91
|
+
# Provides a hook to include or disable customized transactional save behavior.
|
92
|
+
# Override this method to customize the implementation or disable it altogether.
|
93
|
+
# The current implementation in @see DataMapper::NestedAttributes::TransactionalSave
|
94
|
+
# simply wraps the saving of the complete object tree inside a transaction
|
95
|
+
# and rolls back in case any exceptions are raised, or any of the calls to
|
96
|
+
# @see DataMapper::Resource#save returned false
|
97
|
+
#
|
98
|
+
# @return Not specified
|
99
|
+
#
|
100
|
+
def add_transactional_save_behavior
|
101
|
+
require 'dm-accepts_nested_attributes/transactional_save'
|
102
|
+
include ::DataMapper::NestedAttributes::TransactionalSave
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Provides a hook to include or disable customized error collecting behavior.
|
107
|
+
# Overwrite this method to customize the implementation or disable it altogether.
|
108
|
+
# The current implementation in @see DataMapper::NestedAttributes::ValidationErrorCollecting
|
109
|
+
# simply attaches all errors of related resources to the object that was initially saved.
|
110
|
+
#
|
111
|
+
# @return Not specified
|
112
|
+
#
|
113
|
+
def add_error_collection_behavior
|
114
|
+
require 'dm-accepts_nested_attributes/error_collecting'
|
115
|
+
include ::DataMapper::NestedAttributes::ValidationErrorCollecting
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Checks options passed to @see accepts_nested_attributes_for
|
120
|
+
# If any of the given options is invalid, this method will raise
|
121
|
+
# @see DataMapper::NestedAttributes::InvalidOptions
|
122
|
+
#
|
123
|
+
# @param [Hash, nil] options
|
124
|
+
# The options passed to @see accepts_nested_attributes_for
|
125
|
+
#
|
126
|
+
# @raise [DataMapper::NestedAttributes::InvalidOptions]
|
127
|
+
# A named exception class indicating invalid options
|
128
|
+
#
|
129
|
+
# @return [nil]
|
130
|
+
#
|
131
|
+
def assert_valid_options_for_nested_attributes(options)
|
132
|
+
|
133
|
+
assert_kind_of 'options', options, Hash
|
134
|
+
|
135
|
+
valid_options = [ :allow_destroy, :reject_if ]
|
136
|
+
|
137
|
+
unless options.all? { |k,v| valid_options.include?(k) }
|
138
|
+
raise InvalidOptions, 'options must be one of :allow_destroy or :reject_if'
|
139
|
+
end
|
140
|
+
|
141
|
+
guard = options[:reject_if]
|
142
|
+
if guard.is_a?(Symbol) || guard.is_a?(String)
|
143
|
+
msg = ":reject_if => #{guard.inspect}, but there is no instance method #{guard.inspect} in #{self.name}"
|
144
|
+
raise InvalidOptions, msg unless instance_methods.include?(options[:reject_if].to_s)
|
145
|
+
else
|
146
|
+
msg = ":reject_if must be a Symbol|String or respond_to?(:call) "
|
147
|
+
raise InvalidOptions, msg unless guard.nil? || guard.respond_to?(:call)
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|