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/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