on_form 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 05e792fbba20f1c4d82034000b2e49f2f5e01586
4
- data.tar.gz: d8841bae2c6429b08945385a899d89b64e19f3a1
3
+ metadata.gz: da2ced30fe04b528a602ad17e4b6ba170c08c02e
4
+ data.tar.gz: 757f860fa9c1b41c575f6ea5c0a7a4920d32c240
5
5
  SHA512:
6
- metadata.gz: c6557351f0b77e782903e27985c77afd88a6a9872fd506acfb26dd80464fc090337962aa5eead731175a9983fbe9c991590dfd16ae3151eff769927dccafb349
7
- data.tar.gz: 65bd1b58e606758dce47bf865a7debe2bb602286cf79c7d025fd28472223553f54b0a2322a9a319f806ce027f55775827dcab27b3cf0209992e4663bd0b664de
6
+ metadata.gz: d85b8d09675bb0edbd414bb4ed3ddbc44b14bc17a571e6259928099f2e611c56607d2f541c99067653f7f79deeab60a7aee1884d249e70c36a24204f26543733
7
+ data.tar.gz: cd09754e7fe9a5a686911a577aa2e2e85edd478c7f79107b4d7c2601a46d9b9b3158f2d43bb3d0c776074e61fa752ac825498a02af3cbeb9ffc80cc09c40975a
data/CHANGES.md CHANGED
@@ -1,9 +1,15 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ 3.0.0
5
+ -----
6
+ * Support `save(validate: false)` and `save!(validate: false)`.
7
+ * Change callback order to better match ActiveRecord: validation callbacks fire before `before_save` fires, and model callbacks fire before the form's `after_validation` callback fires.
8
+ * Collect errors from collection forms and present them on the form itself. Thanks @Dhamsoft.
9
+
4
10
  2.3.0
5
11
  -----
6
- * Add `take_identity_from` to improve interoperability with standard resource ('RESTful') controllers and form helpers
12
+ * Add `take_identity_from` to improve interoperability with standard resource ('RESTful') controllers and form helpers.
7
13
 
8
14
  2.2.2
9
15
  -----
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2016 Powershop New Zealand Limited
3
+ Copyright (c) 2016-2018 Flux Federation Limited
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -259,14 +259,16 @@ protected
259
259
  end
260
260
  ```
261
261
 
262
- Note that model save calls are nested inside the form save calls, which means that although form validation takes place before form save starts, model validation takes place after form saving begins.
262
+ Model validations and validation callbacks occur between the form validation before and after callbacks, and model save calls are nested inside the form save calls, but the save calls all follow the validations and validation callbacks.
263
263
 
264
264
  form before_validation
265
+ model before_validation
266
+ model validate (validations defined on the model)
267
+ model after_validation
265
268
  form validate (validations defined on the form itself)
269
+ form after_validation
266
270
  form before_save
267
271
  form around_save begins
268
- model before_validation
269
- model validate (validations defined on the model)
270
272
  model before_save
271
273
  model around_save begins
272
274
  model saved
@@ -386,7 +388,7 @@ If you prefer, you can use the Rails `included` block syntax in the module inste
386
388
 
387
389
  After checking out the repo, pick the rails version you'd like to run tests against, and run:
388
390
 
389
- RAILS_VERSION=5.0.0.1 bundle update
391
+ RAILS_VERSION=5.2.0 bundle update
390
392
 
391
393
  You should then be able to run the test suite:
392
394
 
@@ -404,4 +406,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/powers
404
406
 
405
407
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
406
408
 
407
- Copyright © Powershop New Zealand Limited, 2016
409
+ Copyright © Flux Federation Limited, 2016-2018.
@@ -0,0 +1,121 @@
1
+ module OnForm
2
+ class CollectionWrapper
3
+ include ::Enumerable
4
+
5
+ attr_reader :parent, :association_name, :collection_form_class, :allow_insert, :allow_update, :allow_destroy
6
+
7
+ delegate :each, :first, :last, :[], to: :to_a
8
+
9
+ def initialize(parent, association_name, collection_form_class, allow_insert, allow_update, allow_destroy)
10
+ @parent = parent
11
+ @association_name = association_name
12
+ @association = parent.association(association_name)
13
+ @association_proxy = parent.send(association_name)
14
+ @collection_form_class = collection_form_class
15
+ @allow_insert, @allow_update, @allow_destroy = allow_insert, allow_update, allow_destroy
16
+ @wrapped_records = {}
17
+ @wrapped_new_records = []
18
+ @loaded_forms = []
19
+ end
20
+
21
+ def to_ary
22
+ to_a
23
+ end
24
+
25
+ def each
26
+ @association_proxy.each { |record| yield wrapped_record(record) }
27
+ end
28
+
29
+ def size
30
+ @association_proxy.size
31
+ end
32
+
33
+ def save_forms(validate: true)
34
+ @loaded_forms.each do |form|
35
+ if form.marked_for_destruction?
36
+ form.record.destroy
37
+ else
38
+ form.save!(validate: validate)
39
+ end
40
+ end
41
+ end
42
+
43
+ def validate_forms(parent_form)
44
+ @loaded_forms.collect do |form|
45
+ add_errors_to_parent(parent_form, form) if form.invalid?
46
+ end
47
+ end
48
+
49
+ def form_errors?
50
+ @loaded_forms.map(&:form_errors?).any?
51
+ end
52
+
53
+ def reset_forms_errors
54
+ @loaded_forms.collect(&:reset_errors)
55
+ end
56
+
57
+ def parse_collection_attributes(params)
58
+ params = params.values unless params.is_a?(Array)
59
+
60
+ records_to_insert = []
61
+ records_to_update = {}
62
+ records_to_destroy = []
63
+
64
+ params.each do |attributes|
65
+ destroy = self.class.boolean_type.cast(attributes['_destroy']) || self.class.boolean_type.cast(attributes[:_destroy])
66
+ if id = attributes['id'] || attributes[:id]
67
+ if destroy
68
+ records_to_destroy << id.to_i if allow_destroy
69
+ else
70
+ records_to_update[id.to_i] = attributes.except('id', :id, '_destroy', :destroy) if allow_update
71
+ end
72
+ elsif !destroy
73
+ records_to_insert << attributes.except('_destroy', :destroy) if allow_insert
74
+ end
75
+ end
76
+
77
+ to_a if @association_proxy.loaded?
78
+ records_to_load = records_to_update.keys + records_to_destroy - @wrapped_records.keys.collect(&:id)
79
+ @association_proxy.find(records_to_load).each do |record|
80
+ @association.add_to_target(record, :skip_callbacks)
81
+ wrapped_record(record)
82
+ end
83
+ loaded_forms_by_id = @wrapped_records.values.index_by(&:id)
84
+
85
+ records_to_insert.each do |attributes|
86
+ wrapped_record(@association_proxy.build).attributes = attributes
87
+ end
88
+
89
+ records_to_update.each do |id, attributes|
90
+ loaded_forms_by_id[id].attributes = attributes
91
+ end
92
+
93
+ records_to_destroy.each do |id|
94
+ loaded_forms_by_id[id].mark_for_destruction
95
+ end
96
+
97
+ params
98
+ end
99
+
100
+ protected
101
+ def self.boolean_type
102
+ @boolean_type ||= Types.lookup(:boolean, {})
103
+ end
104
+
105
+ def add_errors_to_parent(parent_form, child_form)
106
+ return unless child_form.errors.present?
107
+
108
+ association_exposed_name = child_form.class.identity_model_name.to_s.pluralize
109
+ child_form.errors.each do |attribute, errors|
110
+ Array(errors).each { |error| parent_form.errors["#{association_exposed_name}.#{attribute}"] << error }
111
+ if parent_form.errors["#{association_exposed_name}.#{attribute}"].present?
112
+ parent_form.errors["#{association_exposed_name}.#{attribute}"].uniq!
113
+ end
114
+ end
115
+ end
116
+
117
+ def wrapped_record(record)
118
+ @wrapped_records[record] ||= @collection_form_class.new(record).tap { |form| @loaded_forms << form }
119
+ end
120
+ end
121
+ end
@@ -4,12 +4,17 @@ module OnForm
4
4
  @errors ||= ActiveModel::Errors.new(self)
5
5
  end
6
6
 
7
- private
8
7
  def reset_errors
9
8
  @errors = nil
9
+ reset_errors_on_child_forms
10
+ end
11
+
12
+ private
13
+ def reset_errors_on_child_forms
14
+ collection_wrappers.each_value(&:reset_forms_errors)
10
15
  end
11
16
 
12
- def collect_errors
17
+ def collect_errors_from_backing_model_instances
13
18
  self.class.exposed_attributes.each do |backing_model_name, attribute_mappings|
14
19
  backing_model = backing_model_instance(backing_model_name)
15
20
 
data/lib/on_form/form.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module OnForm
2
2
  class Form
3
3
  include ActiveModel::Validations
4
+ include Validations
4
5
  include ActiveModel::Validations::Callbacks
5
6
 
6
7
  include Attributes
@@ -68,10 +69,33 @@ module OnForm
68
69
  define_method("#{name}=") { |arg| introduced_attribute_values.delete(name); introduced_attribute_values_before_type_cast[name] = arg }
69
70
  end
70
71
 
71
- def self.take_identity_from(backing_model_name)
72
+ def self.take_identity_from(backing_model_name, convert_to_model: true)
72
73
  @identity_model_name = backing_model_name.to_sym
73
74
  expose_backing_model(@identity_model_name)
74
- delegate :to_model, :to_key, :to_param, :persisted?, to: backing_model_name
75
+ delegate :id, :to_key, :to_param, :persisted?, :mark_for_destruction, :_destroy, :marked_for_destruction?, to: backing_model_name
76
+ delegate :to_model, to: backing_model_name if convert_to_model
77
+ end
78
+
79
+ def self.expose_collection_of(association_name, on: nil, prefix: nil, suffix: nil, as: nil, allow_insert: true, allow_update: true, allow_destroy: false, &block)
80
+ exposed_name = as || "#{prefix}#{association_name}#{suffix}"
81
+ singular_name = exposed_name.to_s.singularize
82
+ association_name = association_name.to_sym
83
+
84
+ on = prepare_model_to_expose!(on)
85
+
86
+ collection_form_class = Class.new(OnForm::Form)
87
+ const_set(exposed_name.to_s.classify + "Form", collection_form_class)
88
+
89
+ collection_form_class.send(:define_method, :initialize) { |record| @record = record }
90
+ collection_form_class.send(:attr_reader, :record)
91
+ collection_form_class.send(:alias_method, singular_name, :record)
92
+ collection_form_class.take_identity_from singular_name, convert_to_model: false
93
+ collection_form_class.class_eval(&block)
94
+
95
+ define_method(exposed_name) { collection_wrappers[association_name] ||= CollectionWrapper.new(backing_model_instance(on), association_name, collection_form_class, allow_insert, allow_update, allow_destroy) } # used by action_view's fields_for, and by the following lines
96
+ define_method("#{exposed_name}_attributes=") { |params| send(exposed_name).parse_collection_attributes(params) }
97
+
98
+ collection_form_class
75
99
  end
76
100
 
77
101
  protected
@@ -82,5 +106,16 @@ module OnForm
82
106
  def introduced_attribute_values_before_type_cast
83
107
  @introduced_attribute_values_before_type_cast ||= {}
84
108
  end
109
+
110
+ def collection_wrappers
111
+ @collection_wrappers ||= {}
112
+ end
113
+
114
+ def self.prepare_model_to_expose!(on)
115
+ raise ArgumentError, "must choose the model to expose the attributes on" unless on || identity_model_name
116
+ on = (on || identity_model_name).to_sym
117
+ expose_backing_model(on)
118
+ on
119
+ end
85
120
  end
86
121
  end
@@ -14,31 +14,33 @@ module OnForm
14
14
  end
15
15
  end
16
16
 
17
- def invalid?
18
- !valid?
19
- end
20
-
21
- def save!
22
- reset_errors
17
+ def save!(validate: true)
23
18
  transaction do
24
19
  reset_errors
25
- unless run_validations!(backing_model_validations: false)
26
- raise ActiveModel::ValidationError, self
20
+
21
+ if validate
22
+ run_validations!
23
+
24
+ if !errors.empty?
25
+ if form_errors?
26
+ raise ActiveModel::ValidationError, self
27
+ else
28
+ raise ActiveRecord::RecordInvalid, self
29
+ end
30
+ end
27
31
  end
32
+
28
33
  run_callbacks :save do
29
- begin
30
- backing_model_instances.each { |backing_model| backing_model.save! }
31
- rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
32
- collect_errors
33
- raise
34
- end
34
+ # we pass (validate: false) to avoid running the validations a second time, but we use save! to get the RecordNotFound behavior
35
+ backing_model_instances.each { |backing_model| backing_model.save!(validate: false) }
36
+ save_child_forms(validate: false)
35
37
  end
36
38
  end
37
39
  true
38
40
  end
39
41
 
40
- def save
41
- save!
42
+ def save(validate: true)
43
+ save!(validate: validate)
42
44
  rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
43
45
  false
44
46
  end
@@ -71,15 +73,8 @@ module OnForm
71
73
  end
72
74
  end
73
75
 
74
- def run_validations!(backing_model_validations: true)
75
- super()
76
- run_backing_model_validations if backing_model_validations
77
- errors.empty?
78
- end
79
-
80
- def run_backing_model_validations
81
- backing_model_instances.collect { |backing_model| backing_model.valid? }
82
- collect_errors
76
+ def save_child_forms(validate: true)
77
+ collection_wrappers.each_value {|collection| collection.save_forms(validate: validate) }
83
78
  end
84
79
  end
85
80
  end
@@ -0,0 +1,36 @@
1
+ module OnForm
2
+ module Validations
3
+ def self.included(base)
4
+ base.validate :run_backing_model_validations
5
+ end
6
+
7
+ def invalid?
8
+ !valid?
9
+ end
10
+
11
+ def form_errors?
12
+ !!(@_form_validation_errors || child_form_errors?)
13
+ end
14
+
15
+ private
16
+ def run_backing_model_validations
17
+ backing_model_instances.each { |backing_model| backing_model.valid? }
18
+ end
19
+
20
+ def run_validations!
21
+ super
22
+ @_form_validation_errors = !errors.empty?
23
+ collect_errors_from_backing_model_instances
24
+ run_child_form_validations!
25
+ errors.empty?
26
+ end
27
+
28
+ def run_child_form_validations!
29
+ collection_wrappers.each_value {|collection| collection.validate_forms(self) }
30
+ end
31
+
32
+ def child_form_errors?
33
+ collection_wrappers.values.map(&:form_errors?).any?
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,3 @@
1
1
  module OnForm
2
- VERSION = "2.3.0"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/on_form.rb CHANGED
@@ -4,6 +4,8 @@ require "on_form/version"
4
4
  require "on_form/rails_compat"
5
5
  require "on_form/attributes"
6
6
  require "on_form/errors"
7
+ require "on_form/validations"
7
8
  require "on_form/saving"
8
9
  require "on_form/types"
10
+ require "on_form/collection_wrapper"
9
11
  require "on_form/form"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: on_form
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Will Bryant
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-04 00:00:00.000000000 Z
11
+ date: 2018-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -98,11 +98,13 @@ files:
98
98
  - bin/setup
99
99
  - lib/on_form.rb
100
100
  - lib/on_form/attributes.rb
101
+ - lib/on_form/collection_wrapper.rb
101
102
  - lib/on_form/errors.rb
102
103
  - lib/on_form/form.rb
103
104
  - lib/on_form/rails_compat.rb
104
105
  - lib/on_form/saving.rb
105
106
  - lib/on_form/types.rb
107
+ - lib/on_form/validations.rb
106
108
  - lib/on_form/version.rb
107
109
  - on_form.gemspec
108
110
  homepage: https://github.com/powershop/on_form
@@ -125,9 +127,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
127
  version: '0'
126
128
  requirements: []
127
129
  rubyforge_project:
128
- rubygems_version: 2.5.1
130
+ rubygems_version: 2.5.2
129
131
  signing_key:
130
132
  specification_version: 4
131
133
  summary: A pragmatism-first library to help Rails applications migrate from complex
132
134
  nested attribute models to tidy form objects.
133
135
  test_files: []
136
+ has_rdoc: