dm-accepts_nested_attributes 0.12.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.
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,6 @@
1
+ TODO
2
+ ====
3
+
4
+ * collect validation errors from related resources
5
+ * update README to include more complete usecases
6
+ * think about replacing :reject_if with :if and :unless
@@ -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,7 @@
1
+ require 'dm-core'
2
+
3
+ require 'dm-accepts_nested_attributes/model'
4
+ require 'dm-accepts_nested_attributes/resource'
5
+
6
+ # Activate the plugin
7
+ DataMapper::Model.append_extensions(DataMapper::NestedAttributes::Model)
@@ -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