reform 1.2.0.beta2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,4 +10,4 @@ module Reform
10
10
  :from_json
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -1,23 +1,8 @@
1
1
  module Reform::Form::Save
2
- module RecursiveSave
3
- def to_hash(*)
4
- # process output from InputRepresenter {title: "Mint Car", hit: <Form>}
5
- # and just call sync! on nested forms.
6
- nested_forms do |attr|
7
- attr.merge!(
8
- :instance => lambda { |fragment, *| fragment },
9
- :serialize => lambda { |object, args| object.save! unless args.binding[:save] === false },
10
- )
11
- end
12
-
13
- super
14
- end
15
- end
16
-
17
2
  # Returns the result of that save invocation on the model.
18
3
  def save(options={}, &block)
19
4
  # DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
20
- return deprecate_first_save_block_arg(&block) if block_given?
5
+ return yield to_nested_hash if block_given?
21
6
 
22
7
  sync_models # recursion
23
8
  save!(options)
@@ -25,15 +10,10 @@ module Reform::Form::Save
25
10
 
26
11
  def save!(options={}) # FIXME.
27
12
  result = save_model
28
- mapper.new(fields).extend(RecursiveSave).to_hash # save! on all nested forms. # TODO: only include nested forms here.
29
13
 
30
- names = options.keys & changed.keys.map(&:to_sym)
31
- if names.size > 0
32
- representer = save_dynamic_representer.new(fields) # should be done once, on class instance level.
14
+ save_representer.new(fields).to_hash # save! on all nested forms.
33
15
 
34
- # puts "$$$$$$$$$ #{names.inspect}"
35
- representer.to_hash(options.merge :include => names)
36
- end
16
+ dynamic_save!(options)
37
17
 
38
18
  result
39
19
  end
@@ -43,54 +23,44 @@ module Reform::Form::Save
43
23
  end
44
24
 
45
25
 
46
- def save_dynamic_representer
47
- # puts mapper.superclass.superclass.inspect
48
- Class.new(mapper).apply do |dfn|
26
+ require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
27
+ def to_nested_hash(*)
28
+ ActiveSupport::HashWithIndifferentAccess.new(nested_hash_representer.new(fields).to_hash)
29
+ end
30
+ alias_method :to_hash, :to_nested_hash
31
+ # NOTE: it is not recommended using #to_hash and #to_nested_hash in your code, consider them private.
32
+
33
+ private
34
+ def save_representer
35
+ self.class.representer(:save) do |dfn|
49
36
  dfn.merge!(
50
- :serialize => lambda { |object, options|
51
- puts "$$ #{options.user_options.inspect}"
52
- options.user_options[options.binding.name.to_sym].call(object, options) },
53
- :representable => true
37
+ :instance => lambda { |form, *| form },
38
+ :serialize => lambda { |form, args| form.save! unless args.binding[:save] === false }
54
39
  )
55
40
  end
56
41
  end
57
42
 
43
+ def nested_hash_representer
44
+ self.class.representer(:nested_hash, :all => true) do |dfn|
45
+ dfn.merge!(:serialize => lambda { |form, args| form.to_nested_hash }) if dfn[:form]
58
46
 
59
- module NestedHash
60
- def to_hash(*)
61
- # Transform form data into a nested hash for #save.
62
- nested_forms do |attr|
63
- attr.merge!(
64
- :serialize => lambda { |object, args| object.to_nested_hash }
65
- )
66
- end
67
-
68
- representable_attrs.each do |attr|
69
- attr.merge!(:as => attr[:private_name] || attr.name)
70
- end
71
-
72
- super
47
+ dfn.merge!(:as => dfn[:private_name] || dfn.name)
73
48
  end
74
49
  end
75
50
 
51
+ def dynamic_save!(options)
52
+ names = options.keys & changed.keys.map(&:to_sym)
53
+ return if names.size == 0
76
54
 
77
- require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
78
- def to_nested_hash(*)
79
- map = mapper.new(fields).extend(NestedHash)
80
-
81
- ActiveSupport::HashWithIndifferentAccess.new(map.to_hash)
55
+ dynamic_save_representer.new(fields).to_hash(options.merge(:include => names))
82
56
  end
83
- alias_method :to_hash, :to_nested_hash
84
- # NOTE: it is not recommended using #to_hash and #to_nested_hash in your code, consider
85
- # them private.
86
57
 
87
- private
88
- def deprecate_first_save_block_arg(&block)
89
- if block.arity == 2
90
- warn "[Reform] Deprecation Warning: The first block argument in `save { |form, hash| .. }` is deprecated and its new signature is `save { |hash| .. }`. If you need the form instance, use it in the block. Have a good day."
91
- return yield(self, to_nested_hash)
58
+ def dynamic_save_representer
59
+ self.class.representer(:dynamic_save, :all => true) do |dfn|
60
+ dfn.merge!(
61
+ :serialize => lambda { |object, options| options.user_options[options.binding.name.to_sym].call(object, options) },
62
+ :representable => true
63
+ )
92
64
  end
93
-
94
- yield to_nested_hash # new behaviour.
95
65
  end
96
66
  end
@@ -2,113 +2,95 @@
2
2
  # 1. assign scalars to model (respecting virtual, excluded attributes)
3
3
  # 2. call sync! on nested
4
4
  module Reform::Form::Sync
5
- # Mechanics for writing input to model.
6
- # Writes input to model.
7
- module Writer
8
- def from_hash(*)
9
- # process output from InputRepresenter {title: "Mint Car", hit: <Form>}
10
- # and just call sync! on nested forms.
11
- nested_forms do |attr|
12
- attr.merge!(
13
- :instance => lambda { |fragment, *| fragment },
14
- # FIXME: do we allow options for #sync for nested forms?
15
- :deserialize => lambda { |object, *| model = object.sync!({}) } # sync! returns the synced model.
16
- # representable's :setter will do collection=([..]) or property=(..) for us on the model.
17
- )
18
- end
19
-
20
- super
21
- end
5
+ def sync_models(options={})
6
+ sync!(options)
22
7
  end
8
+ alias_method :sync, :sync_models
23
9
 
24
- module Setter
25
- def from_hash(*)
26
- clone_config!
10
+ # reading from fields allows using readers in form for presentation
11
+ # and writers still pass to fields in #validate????
12
+ def sync!(options) # semi-public.
13
+ options = Reform::Representer::Options[options.merge(:form => self)] # options local for this form, only.
27
14
 
28
- representable_attrs.each do |dfn|
29
- next unless setter = dfn[:sync]
15
+ input = sync_hash(options)
16
+ # if aliased_model was a proper Twin, we could do changed? stuff there.
30
17
 
31
- setter_proc = lambda do |value, options|
32
- # puts "~~ #{value}~ #{options.user_options.inspect}"
18
+ options.delete(:exclude) # TODO: can we use 2 options?
33
19
 
34
- if options.binding[:sync] == true
35
- options.user_options[options.binding.name.to_sym].call(value, options)
36
- next
37
- end
20
+ dynamic_sync_representer.new(aliased_model).from_hash(input, options) # sync properties to Song.
38
21
 
39
- # evaluate the :sync block in form context (should we do that everywhere?).
40
- options.user_options[:form].instance_exec(value, options, &setter)
41
- end
22
+ model
23
+ end
42
24
 
43
- dfn.merge!(:setter => setter_proc)
44
- end
25
+ private
45
26
 
46
- super
27
+ # Transforms form input into what actually gets written to model.
28
+ # output: {title: "Mint Car", hit: <Form>}
29
+ def input_representer
30
+ self.class.representer(:input) do |dfn|
31
+ dfn.merge!(
32
+ :representable => false,
33
+ :prepare => lambda { |obj, *| obj }
34
+ )
47
35
  end
48
36
  end
49
37
 
50
- # Transforms form input into what actually gets written to model.
51
- # output: {title: "Mint Car", hit: <Form>}
52
- module InputRepresenter
53
- # receives Representer::Options hash.
54
- def to_hash(*)
55
- nested_forms do |attr|
56
- attr.merge!(
57
- :representable => false,
58
- :prepare => lambda { |obj, *| obj }
38
+ # Writes input to model.
39
+ def sync_representer
40
+ self.class.representer(:sync, :all => true) do |dfn|
41
+ if dfn[:form]
42
+ dfn.merge!(
43
+ :instance => lambda { |fragment, *| fragment }, # use model's nested property for syncing.
44
+ # FIXME: do we allow options for #sync for nested forms?
45
+ :deserialize => lambda { |object, *| model = object.sync!({}) } # sync! returns the synced model.
46
+ # representable's :setter will do collection=([..]) or property=(..) for us on the model.
59
47
  )
60
48
  end
61
-
62
- super
63
49
  end
64
50
  end
65
51
 
52
+ # This representer inherits from sync_representer and add functionality on top of that.
53
+ # It allows running custom dynamic blocks for properties when syncing.
54
+ def dynamic_sync_representer
55
+ self.class.representer(:dynamic_sync, superclass: sync_representer, :all => true) do |dfn|
56
+ next unless setter = dfn[:sync]
66
57
 
67
- def sync_models(options={})
68
- sync!(options)
69
- end
70
- alias_method :sync, :sync_models
71
-
72
- # reading from fields allows using readers in form for presentation
73
- # and writers still pass to fields in #validate????
74
- def sync!(options) # semi-public.
75
- options = Reform::Representer::Options[options.merge(:form => self)] # options local for this form, only.
76
-
77
- input = sync_hash(options)
78
- # if aliased_model was a proper Twin, we could do changed? stuff there.
79
- # setter_module = Class.new(self.class.representer_class)
80
- # setter_module.send :include, Setter
81
-
82
- options.delete(:exclude) # TODO: can we use 2 options?
58
+ setter_proc = lambda do |value, options|
59
+ if options.binding[:sync] == true # sync: true will call the runtime lambda from the options hash.
60
+ options.user_options[options.binding.name.to_sym].call(value, options)
61
+ next
62
+ end
83
63
 
84
- mapper.new(aliased_model).extend(Writer).extend(Setter).from_hash(input, options) # sync properties to Song.
64
+ # evaluate the :sync block in form context (should we do that everywhere?).
65
+ options.user_options[:form].instance_exec(value, options, &setter)
66
+ end
85
67
 
86
- model
68
+ dfn.merge!(:setter => setter_proc)
69
+ end
87
70
  end
88
71
 
89
- private
72
+
90
73
  # API: semi-public.
91
74
  module SyncHash
92
75
  # This hash goes into the Writer that writes properties back to the model. It only contains "writeable" attributes.
93
76
  def sync_hash(options)
94
- input_representer = mapper.new(fields).extend(InputRepresenter)
95
- input_representer.to_hash(options)
77
+ input_representer.new(fields).to_hash(options)
96
78
  end
97
79
  end
98
80
  include SyncHash
99
81
 
100
82
 
101
- # Excludes :virtual properties from #sync in this form.
102
- module ReadOnly
83
+ # Excludes :virtual and readonly properties from #sync in this form.
84
+ module Writeable
103
85
  def sync_hash(options)
104
- readonly_fields = mapper.fields { |dfn| dfn[:virtual] }
86
+ readonly_fields = mapper.fields { |dfn| dfn[:_writeable] == false }
105
87
 
106
88
  options.exclude!(readonly_fields.map(&:to_sym))
107
89
 
108
90
  super
109
91
  end
110
92
  end
111
- include ReadOnly
93
+ include Writeable
112
94
 
113
95
 
114
96
  # This will skip unchanged properties in #sync. To use this for all nested form do as follows.
@@ -1,48 +1,5 @@
1
1
  # Mechanics for writing to forms in #validate.
2
2
  module Reform::Form::Validate
3
- module Update
4
- # IDEA: what if Populate was a Decorator that simply knows how to setup the Form object graph, nothing more? That would decouple
5
- # the population from the validation (good and bad as less customizable).
6
-
7
- # Go through all nested forms and call form.update!(hash).
8
- def from_hash(*)
9
- nested_forms do |attr|
10
- attr.merge!(
11
- # set parse_strategy: sync> # DISCUSS: that kills the :setter directive, which usually sucks. at least document this in :populator.
12
- :collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
13
- :parse_strategy => :sync, # just use nested objects as they are.
14
-
15
- # :getter grabs nested forms directly from fields bypassing the reader method which could possibly be overridden for presentation.
16
- :getter => lambda { |options| fields.send(options.binding.name) },
17
- :deserialize => lambda { |object, params, args| object.update!(params) },
18
- )
19
-
20
- # TODO: :populator now is just an alias for :instance. handle in ::property.
21
- attr.merge!(:instance => attr[:populator]) if attr[:populator]
22
-
23
- attr.merge!(:instance => Populator::PopulateIfEmpty.new) if attr[:populate_if_empty]
24
- end
25
-
26
- # FIXME: solve this with a dedicated Populate Decorator per Form.
27
- representable_attrs.each do |attr|
28
- attr.merge!(:parse_filter => Representable::Coercion::Coercer.new(attr[:coercion_type])) if attr[:coercion_type]
29
-
30
- attr.merge!(:skip_if => Skip::AllBlank.new) if attr[:skip_if] == :all_blank
31
- attr.merge!(:skip_parse => attr[:skip_if]) if attr[:skip_if]
32
- end
33
-
34
-
35
- representable_attrs.each do |attr|
36
- next if attr[:form]
37
-
38
- attr.merge!(:parse_filter => Changed.new)
39
- end
40
-
41
- super
42
- end
43
- end
44
-
45
-
46
3
  module Populator
47
4
  # This might change soon (e.g. moved into disposable).
48
5
  class PopulateIfEmpty
@@ -130,10 +87,46 @@ private
130
87
  def deserialize!(params)
131
88
  # using self here will call the form's setters like title= which might be overridden.
132
89
  # from_hash(params, parent_form: self)
133
- mapper.new(self).extend(Update).send(deserialize_method, params, :parent_form => self)
90
+ # Go through all nested forms and call form.update!(hash).
91
+ populate_representer.new(self).send(deserialize_method, params, :parent_form => self)
134
92
  end
135
93
 
136
94
  def deserialize_method
137
95
  :from_hash
138
96
  end
97
+
98
+ # IDEA: what if Populate was a Decorator that simply knows how to setup the Form object graph, nothing more? That would decouple
99
+ # the population from the validation (good and bad as less customizable).
100
+
101
+ # Don't get scared by this method. All this does is create a new representer class for this form.
102
+ # It then configures each property so the population of the form can happen in #validate.
103
+ # A lot of this code is simply renaming from Reform's API to representable's. # FIXME: unify that?
104
+ def populate_representer
105
+ self.class.representer(:populate, :all => true) do |dfn|
106
+ if dfn[:form]
107
+ dfn.merge!(
108
+ # set parse_strategy: sync> # DISCUSS: that kills the :setter directive, which usually sucks. at least document this in :populator.
109
+ :collection => dfn[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in dfn YET.
110
+ :parse_strategy => :sync, # just use nested objects as they are.
111
+
112
+ # :getter grabs nested forms directly from fields bypassing the reader method which could possibly be overridden for presentation.
113
+ :getter => lambda { |options| fields.send(options.binding.name) },
114
+ :deserialize => lambda { |object, params, args| object.update!(params) },
115
+ )
116
+
117
+ # TODO: :populator now is just an alias for :instance. handle in ::property.
118
+ dfn.merge!(:instance => dfn[:populator]) if dfn[:populator]
119
+
120
+ dfn.merge!(:instance => Populator::PopulateIfEmpty.new) if dfn[:populate_if_empty]
121
+ end
122
+
123
+
124
+ dfn.merge!(:parse_filter => Representable::Coercion::Coercer.new(dfn[:coercion_type])) if dfn[:coercion_type]
125
+
126
+ dfn.merge!(:skip_if => Skip::AllBlank.new) if dfn[:skip_if] == :all_blank
127
+ dfn.merge!(:skip_parse => dfn[:skip_if]) if dfn[:skip_if]
128
+
129
+ dfn.merge!(:parse_filter => Changed.new) unless dfn[:form] # TODO: make changed? work for nested forms.
130
+ end
131
+ end
139
132
  end
@@ -38,10 +38,12 @@ module Reform
38
38
  representable_attrs.find_all(&block).map(&:name)
39
39
  end
40
40
 
41
- def nested_forms(&block)
42
- clone_config!.
43
- find_all { |attr| attr[:form] }.
44
- each(&block)
41
+ def self.each(only_form=true, &block)
42
+ definitions = representable_attrs
43
+ definitions = representable_attrs.find_all { |attr| attr[:form] } if only_form
44
+
45
+ definitions.each(&block)
46
+ self
45
47
  end
46
48
 
47
49
  def self.for(options)
@@ -59,12 +61,6 @@ module Reform
59
61
  end
60
62
 
61
63
  private
62
- def clone_config!
63
- # TODO: representable_attrs.clone! which does exactly what's done below.
64
- attrs = Representable::Config.new
65
- attrs.inherit!(representable_attrs) # since in every use case we modify Config we clone.
66
- @representable_attrs = attrs
67
- end
68
64
 
69
65
  # Inline forms always get saved in :extend.
70
66
  def self.build_inline(base, features, name, options, &block)
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "1.2.0.beta2"
2
+ VERSION = "1.2.1"
3
3
  end
@@ -105,6 +105,7 @@ end
105
105
 
106
106
  class PopulateWithActiveRecordTest < MiniTest::Spec
107
107
  class AlbumForm < Reform::Form
108
+
108
109
  property :title
109
110
 
110
111
  collection :songs, :populate_if_empty => Song do
@@ -188,6 +189,73 @@ class PopulateWithActiveRecordTest < MiniTest::Spec
188
189
  album.songs[1].title.must_equal "Check For A Pulse"
189
190
  album.songs[1].persisted?.must_equal true # TODO: with << strategy, this shouldn't be saved.
190
191
  end
192
+
193
+ describe 'using nested_models_attributes to modify nested collection' do
194
+ class ActiveModelAlbumForm < Reform::Form
195
+ include Reform::Form::ActiveModel
196
+ include Reform::Form::ActiveModel::FormBuilderMethods
197
+
198
+ property :title
199
+
200
+ collection :songs, :populate_if_empty => Song do
201
+ property :title
202
+ end
203
+ end
204
+
205
+ let (:album) { Album.create(:title => 'Greatest Hits') }
206
+ let (:form) { ActiveModelAlbumForm.new(album) }
207
+
208
+ it do
209
+ form.validate('songs_attributes' => {'0' => {'title' => 'Tango'}})
210
+
211
+ # form populated.
212
+ form.songs.size.must_equal 1
213
+ form.songs[0].model.must_be_kind_of Song
214
+ form.songs[0].title.must_equal 'Tango'
215
+
216
+ # model NOT populated.
217
+ album.songs.must_equal []
218
+
219
+ form.save
220
+
221
+ # nested model persisted.
222
+ first_song = album.songs[0]
223
+ assert first_song.id > 0
224
+
225
+ # form populated.
226
+ form.songs.size.must_equal 1
227
+
228
+ # model also populated.
229
+ album.songs.size.must_equal 1
230
+ album.songs[0].title.must_equal 'Tango'
231
+
232
+ form.validate('songs_attributes' => {'0' => {'id' => first_song.id, 'title' => 'Tango nuevo'}, '1' => {'title' => 'Waltz'}})
233
+
234
+ # form populated.
235
+ form.songs.size.must_equal 2
236
+ form.songs[0].model.must_be_kind_of Song
237
+ form.songs[1].model.must_be_kind_of Song
238
+ form.songs[0].title.must_equal 'Tango nuevo'
239
+ form.songs[1].title.must_equal 'Waltz'
240
+
241
+ # model NOT populated.
242
+ album.songs.size.must_equal 1
243
+ album.songs[0].title.must_equal 'Tango'
244
+
245
+ form.save
246
+
247
+ # form populated.
248
+ form.songs.size.must_equal 2
249
+
250
+ # model also populated.
251
+ album.songs.size.must_equal 2
252
+ album.songs[0].id.must_equal first_song.id
253
+ album.songs[0].persisted?.must_equal true
254
+ album.songs[1].persisted?.must_equal true
255
+ album.songs[0].title.must_equal 'Tango nuevo'
256
+ album.songs[1].title.must_equal 'Waltz'
257
+ end
258
+ end
191
259
  end
192
260
 
193
261
  # it do