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 +4 -4
- data/CHANGES.md +7 -1
- data/LICENSE.txt +1 -1
- data/README.md +7 -5
- data/lib/on_form/collection_wrapper.rb +121 -0
- data/lib/on_form/errors.rb +7 -2
- data/lib/on_form/form.rb +37 -2
- data/lib/on_form/saving.rb +20 -25
- data/lib/on_form/validations.rb +36 -0
- data/lib/on_form/version.rb +1 -1
- data/lib/on_form.rb +2 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da2ced30fe04b528a602ad17e4b6ba170c08c02e
|
4
|
+
data.tar.gz: 757f860fa9c1b41c575f6ea5c0a7a4920d32c240
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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.
|
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 ©
|
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
|
data/lib/on_form/errors.rb
CHANGED
@@ -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
|
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 :
|
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
|
data/lib/on_form/saving.rb
CHANGED
@@ -14,31 +14,33 @@ module OnForm
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
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
|
-
|
26
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
75
|
-
|
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
|
data/lib/on_form/version.rb
CHANGED
data/lib/on_form.rb
CHANGED
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:
|
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:
|
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.
|
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:
|