forme 1.10.0 → 1.11.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.
- checksums.yaml +4 -4
- data/CHANGELOG +4 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +164 -4
- data/lib/forme/version.rb +1 -1
- data/lib/roda/plugins/forme_route_csrf.rb +15 -1
- data/lib/roda/plugins/forme_set.rb +214 -0
- data/lib/sequel/plugins/forme.rb +3 -3
- data/lib/sequel/plugins/forme_set.rb +47 -27
- data/spec/rails_integration_spec.rb +1 -0
- data/spec/roda_integration_spec.rb +348 -0
- data/spec/sequel_set_plugin_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- metadata +25 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee6b65bc378fc1ea557f3280b5782527735c561e58344e20920a1e728ce4592f
|
4
|
+
data.tar.gz: 4e2b963f58361fb962034c1727f122f22414c7bfcac8f80a969dfa216187ebc3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e40c6db1223714f80e6d603dcd25c48b3baa6ecc89126738f495b9ffc68b84412697b1874c248d8c87c94916b6be6325f47604be17da9c8be390fcced572a1ab
|
7
|
+
data.tar.gz: 5a5ed453fee72ac73348c5fed4083404758d2dbe979d0ca28fb72a50c125e167804e9e55b5ca1491f3e79bc860991e98f5926b35be82e47240763ed7a47a7ef9
|
data/CHANGELOG
CHANGED
data/MIT-LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -589,7 +589,7 @@ As you can see, you basically need to recreate the conditionals used when creati
|
|
589
589
|
the form, so that that the processing of the form submission handles only the
|
590
590
|
inputs that were displayed on the form.
|
591
591
|
|
592
|
-
=== forme_set plugin
|
592
|
+
=== forme_set Sequel plugin
|
593
593
|
|
594
594
|
The forme_set plugin is designed to make handling form submissions easier. What it does
|
595
595
|
is record the form fields that are used on the object, and then it uses those fields
|
@@ -686,6 +686,166 @@ internally). forme_parse returns a hash with the following keys:
|
|
686
686
|
It is possible to use forme_set for the values it can handle, and set other fields manually
|
687
687
|
using set_fields.
|
688
688
|
|
689
|
+
=== forme_set Roda plugin
|
690
|
+
|
691
|
+
The forme_set Roda plugin builds on the forme_set Sequel plugin and is designed to make
|
692
|
+
handling form submissions even easier. This plugin uses a hidden form input to store which
|
693
|
+
fields were used to build the form, as well as some other metadata. It uses another hidden
|
694
|
+
form input with an HMAC, so that on submission, if the HMAC matches, you can be sure that an
|
695
|
+
attacker didn't add extra fields.
|
696
|
+
|
697
|
+
There are a couple advantages to this plugin over using just the Sequel forme_set plugin.
|
698
|
+
One is that you do not need to record the form fields when processing the submission of a
|
699
|
+
form, since the information you need is included in the form submission. Another is that
|
700
|
+
calling the forme_set method is simpler, since it can determine the necessary parameters.
|
701
|
+
|
702
|
+
While you need code like this when using just the Sequel forme_set plugin:
|
703
|
+
|
704
|
+
album = Album[1]
|
705
|
+
Forme.form(album, :action=>'/foo') do |f|
|
706
|
+
f.input :name
|
707
|
+
f.input :copies_sold if album.released?
|
708
|
+
end
|
709
|
+
album.forme_set(params['album'])
|
710
|
+
|
711
|
+
when you also use the Roda forme_set plugin, you can simplify it to:
|
712
|
+
|
713
|
+
album = Album[1]
|
714
|
+
forme_set(album)
|
715
|
+
|
716
|
+
==== Validations
|
717
|
+
|
718
|
+
The Roda forme_set plugin supports and uses the same validations as the Sequel forme_set
|
719
|
+
plugin. However, the Roda plugin is more accurate because it uses the options that were
|
720
|
+
present on the form when it was originally built, instead of the options that would be
|
721
|
+
present on the form when the form was submitted. However, note that that can be a
|
722
|
+
negative if you are dynamically adding values to both the database and the form between
|
723
|
+
when the form was built and when it was submitted.
|
724
|
+
|
725
|
+
==== Usage
|
726
|
+
|
727
|
+
Because the Roda forme_set plugin includes the metadata needed to process the form in form
|
728
|
+
submissions, you don't need to rearrange code to use it, or rerender templates.
|
729
|
+
You can do:
|
730
|
+
|
731
|
+
album = Album[1]
|
732
|
+
forme_set(album)
|
733
|
+
|
734
|
+
And the method will update the +album+ object using the appropriate form values.
|
735
|
+
|
736
|
+
Note that using the Roda forme_set plugin requires you set a secret for the HMAC. It
|
737
|
+
is important that you keep this value secret, because if an attacker has access to this,
|
738
|
+
they would be able to set arbitrary attributes for model objects. In your Roda class,
|
739
|
+
you can load the plugin via:
|
740
|
+
|
741
|
+
plugin :forme_set, :secret => ENV["APP_FORME_HMAC_SECRET"]
|
742
|
+
|
743
|
+
By default, invalid form submissions will raise an exception. If you want to change
|
744
|
+
that behavior (i.e. to display a nice error page), pass a block when loading the plugin:
|
745
|
+
|
746
|
+
plugin :forme_set do |error_type, obj|
|
747
|
+
# ...
|
748
|
+
end
|
749
|
+
|
750
|
+
The block arguments will be a symbol for the type of error (:missing_data, :missing_hmac,
|
751
|
+
:hmac_mismatch, :csrf_mismatch, or :missing_namespace) and the object passed to +forme_set+.
|
752
|
+
This block should raise or halt. If it does not, the default behavior of raising an
|
753
|
+
exception will be taken.
|
754
|
+
|
755
|
+
=== Form Versions
|
756
|
+
|
757
|
+
The Roda forme_set plugin supports form versions. This allows you to gracefully handle
|
758
|
+
changes to forms, processing submissions of the form generated before the change (if
|
759
|
+
possible) as well as the processing submissions of the form generated after the change.
|
760
|
+
|
761
|
+
For example, maybe you have an existing form with just an input for the name:
|
762
|
+
|
763
|
+
form(album) do |f|
|
764
|
+
f.input(:name)
|
765
|
+
end
|
766
|
+
|
767
|
+
Then later, you want to add an input for the number of copies sold:
|
768
|
+
|
769
|
+
form(album) do |f|
|
770
|
+
f.input(:name)
|
771
|
+
f.input(:copies_sold)
|
772
|
+
end
|
773
|
+
|
774
|
+
Using the Roda forme_set plugin, submissions of the old form would only set the
|
775
|
+
name field, it wouldn't set the copies_sold field, since when the form was created,
|
776
|
+
only the name field was used.
|
777
|
+
|
778
|
+
You can handle this case be versioning the form when making changes to it:
|
779
|
+
|
780
|
+
form(album, {}, :form_version=>1) do |f|
|
781
|
+
f.input(:name)
|
782
|
+
f.input(:copies_sold)
|
783
|
+
end
|
784
|
+
|
785
|
+
When you are processing the form submission with forme_set, you pass a block, which
|
786
|
+
will be yielded the version for the form (nil if no version was set):
|
787
|
+
|
788
|
+
forme_set(album) do |version|
|
789
|
+
if version == nil
|
790
|
+
album.copies_sold = 0
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
The block is also yielded the object passed for forme_set, useful if you don't keep
|
795
|
+
a reference to it:
|
796
|
+
|
797
|
+
album = forme_set(Album.new) do |version, obj|
|
798
|
+
if version == nil
|
799
|
+
obj.copies_sold = 0
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
You only need to support old versions of the form for as long as their could be
|
804
|
+
active sessions that could use the old versions of the form. As long you as
|
805
|
+
are expiring sessions to prevent session fixation, you can remove the version
|
806
|
+
handling after the expiration period has passed since the change to the form
|
807
|
+
was made.
|
808
|
+
|
809
|
+
Note that this issue with handling changes to forms is not specific to the Roda
|
810
|
+
forme_set plugin, it affects pretty much all form submissions. The Roda forme_set
|
811
|
+
plugin just makes this issue easier to handle.
|
812
|
+
|
813
|
+
==== Caveats
|
814
|
+
|
815
|
+
The Roda forme_set plugin has basically the same caveats as Sequel forme_set plugin.
|
816
|
+
Additionally, it has a couple other restrictions that the Sequel forme_set plugin
|
817
|
+
does not have.
|
818
|
+
|
819
|
+
First, the Roda forme_set plugin only handles a single object in forms,
|
820
|
+
which must be provided when creating the form. It does not handle multiple
|
821
|
+
objects in the same form, and ignores any fields set for an object different
|
822
|
+
from the one passed when creating the form. You can use the Sequel forme_set
|
823
|
+
plugin to handle form submissions involving multiple objects, or for the
|
824
|
+
objects that were not passed when creating the form.
|
825
|
+
|
826
|
+
Second, the Roda forme_set plugin does not handle cases where the field values
|
827
|
+
are placed outside the forms default namespace. The Sequel forme_set plugin
|
828
|
+
can handle those issues, as long as all values are in the same namespace, since
|
829
|
+
the Sequel forme_set plugin requires you pass in the specific hash to use (the
|
830
|
+
Roda forme_set plugin use the form's namespace information and the submitted
|
831
|
+
parameters to determine the hash to use).
|
832
|
+
|
833
|
+
In cases where the Roda forme_set does not handle things correctly, you can use
|
834
|
+
forme_parse, which will return metadata in the same format as the Sequel plugin
|
835
|
+
forme_parse method, with the addition of a :form_version key in the hash for the
|
836
|
+
form version.
|
837
|
+
|
838
|
+
It is possible to use the Roda forme_set plugin for the submissions it can handle, the
|
839
|
+
Sequel forme_set plugin for the submissions it can handle, and set other fields manually
|
840
|
+
using the Sequel set_fields methods.
|
841
|
+
|
842
|
+
Note that when using the Roda forme_set plugin with an existing form, you should first
|
843
|
+
enable the Roda plugin without actually using the Roda forme_set method. Do not
|
844
|
+
start using the Roda forme_set method until all currently valid sessions were
|
845
|
+
established after the Roda forme_set plugin was enabled. Otherwise, sessions that
|
846
|
+
access the form before the Roda forme_set plugin was enabled will not work if they
|
847
|
+
submit the form after the Roda forme_set plugin is enabled.
|
848
|
+
|
689
849
|
== Other Sequel Plugins
|
690
850
|
|
691
851
|
In addition to the Sequel plugins mentioned above, Forme also ships with additional Sequel
|
@@ -695,9 +855,9 @@ forme_i18n :: Handles translations for labels using i18n.
|
|
695
855
|
|
696
856
|
= Roda Support
|
697
857
|
|
698
|
-
Forme ships with
|
699
|
-
recommended to use forme_route_csrf, as that uses Roda's route_csrf
|
700
|
-
supports more secure request-specific CSRF tokens. In both cases, usage in ERB
|
858
|
+
Forme ships with three Roda plugins: forme_set (discussed above), forme, and forme_route_csrf.
|
859
|
+
For new code, it is recommended to use forme_route_csrf, as that uses Roda's route_csrf
|
860
|
+
plugin, which supports more secure request-specific CSRF tokens. In both cases, usage in ERB
|
701
861
|
templates is the same:
|
702
862
|
|
703
863
|
<% form(@obj, :action=>'/foo') do |f| %>
|
data/lib/forme/version.rb
CHANGED
@@ -45,12 +45,26 @@ class Roda
|
|
45
45
|
csrf_token
|
46
46
|
end
|
47
47
|
|
48
|
+
options[:csrf] = [csrf_field, token]
|
48
49
|
options[:hidden_tags] ||= []
|
49
50
|
options[:hidden_tags] += [{csrf_field=>token}]
|
50
51
|
end
|
51
52
|
|
52
53
|
options[:output] = @_out_buf if block
|
53
|
-
|
54
|
+
_forme_form_options(options)
|
55
|
+
_forme_form_class.form(obj, attr, opts, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# The class to use for forms
|
61
|
+
def _forme_form_class
|
62
|
+
::Forme::ERB::Form
|
63
|
+
end
|
64
|
+
|
65
|
+
# The options to use for forms. Any changes should mutate this hash to set options.
|
66
|
+
def _forme_form_options(options)
|
67
|
+
options
|
54
68
|
end
|
55
69
|
end
|
56
70
|
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'rack/utils'
|
4
|
+
require 'forme/erb_form'
|
5
|
+
|
6
|
+
class Roda
|
7
|
+
module RodaPlugins
|
8
|
+
module FormeSet
|
9
|
+
# Require the forme_route_csrf plugin.
|
10
|
+
def self.load_dependencies(app, _ = nil)
|
11
|
+
app.plugin :forme_route_csrf
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set the HMAC secret.
|
15
|
+
def self.configure(app, opts = OPTS, &block)
|
16
|
+
app.opts[:forme_set_hmac_secret] = opts[:secret] || app.opts[:forme_set_hmac_secret]
|
17
|
+
|
18
|
+
if block
|
19
|
+
app.send(:define_method, :_forme_set_handle_error, &block)
|
20
|
+
app.send(:private, :_forme_set_handle_error)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Error class raised for invalid form submissions.
|
25
|
+
class Error < StandardError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Map of error types to error messages
|
29
|
+
ERROR_MESSAGES = {
|
30
|
+
:missing_data=>"_forme_set_data parameter not submitted",
|
31
|
+
:missing_hmac=>"_forme_set_data_hmac parameter not submitted",
|
32
|
+
:hmac_mismatch=>"_forme_set_data_hmac does not match _forme_set_data",
|
33
|
+
:csrf_mismatch=>"_forme_set_data CSRF token does not match submitted CSRF token",
|
34
|
+
:missing_namespace=>"no content in expected namespace"
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
# Forme::Form subclass that adds hidden fields with metadata that can be used
|
38
|
+
# to automatically process form submissions.
|
39
|
+
class Form < ::Forme::ERB::Form
|
40
|
+
def initialize(obj, opts=nil)
|
41
|
+
super
|
42
|
+
@forme_namespaces = @opts[:namespace]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Try adding hidden fields to all forms
|
46
|
+
def form(*)
|
47
|
+
if block_given?
|
48
|
+
super do |f|
|
49
|
+
yield f
|
50
|
+
hmac_hidden_fields
|
51
|
+
end
|
52
|
+
else
|
53
|
+
t = super
|
54
|
+
if tags = hmac_hidden_fields
|
55
|
+
tags.each{|tag| t << tag}
|
56
|
+
end
|
57
|
+
t
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Add hidden fields with metadata, if the form has an object associated that
|
64
|
+
# supports the forme_inputs method, and it includes inputs.
|
65
|
+
def hmac_hidden_fields
|
66
|
+
if (obj = @opts[:obj]) && obj.respond_to?(:forme_inputs) && (forme_inputs = obj.forme_inputs)
|
67
|
+
columns = []
|
68
|
+
valid_values = {}
|
69
|
+
|
70
|
+
forme_inputs.each do |field, input|
|
71
|
+
next unless col = obj.send(:forme_column_for_input, input)
|
72
|
+
col = col.to_s
|
73
|
+
columns << col
|
74
|
+
|
75
|
+
next unless validation = obj.send(:forme_validation_for_input, field, input)
|
76
|
+
validation[0] = validation[0].to_s
|
77
|
+
has_nil = false
|
78
|
+
validation[1] = validation[1].map do |v|
|
79
|
+
has_nil ||= v.nil?
|
80
|
+
v.to_s
|
81
|
+
end
|
82
|
+
validation[1] << nil if has_nil
|
83
|
+
valid_values[col] = validation
|
84
|
+
end
|
85
|
+
|
86
|
+
return if columns.empty?
|
87
|
+
|
88
|
+
data = {}
|
89
|
+
data['columns'] = columns
|
90
|
+
data['namespaces'] = @forme_namespaces
|
91
|
+
data['csrf'] = @opts[:csrf]
|
92
|
+
data['valid_values'] = valid_values unless valid_values.empty?
|
93
|
+
data['form_version'] = @opts[:form_version] if @opts[:form_version]
|
94
|
+
|
95
|
+
data = data.to_json
|
96
|
+
tags = []
|
97
|
+
tags << tag(:input, :type=>:hidden, :name=>:_forme_set_data, :value=>data)
|
98
|
+
tags << tag(:input, :type=>:hidden, :name=>:_forme_set_data_hmac, :value=>OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, @opts[:roda].class.opts[:forme_set_hmac_secret], data))
|
99
|
+
tags.each{|tag| emit(tag)}
|
100
|
+
tags
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
module InstanceMethods
|
106
|
+
# Return hash based on submitted parameters, with :values key
|
107
|
+
# being submitted values for the object, and :validations key
|
108
|
+
# being a hash of validation metadata for the object.
|
109
|
+
def forme_parse(obj)
|
110
|
+
h = _forme_parse(obj)
|
111
|
+
|
112
|
+
params = h.delete(:params)
|
113
|
+
columns = h.delete(:columns)
|
114
|
+
h[:validations] ||= {}
|
115
|
+
|
116
|
+
values = h[:values] = {}
|
117
|
+
columns.each do |col|
|
118
|
+
values[col.to_sym] = params[col]
|
119
|
+
end
|
120
|
+
|
121
|
+
h
|
122
|
+
end
|
123
|
+
|
124
|
+
# Set fields on the object based on submitted parameters, as
|
125
|
+
# well as validations for associated object values.
|
126
|
+
def forme_set(obj)
|
127
|
+
h = _forme_parse(obj)
|
128
|
+
|
129
|
+
obj.set_fields(h[:params], h[:columns])
|
130
|
+
|
131
|
+
if h[:validations]
|
132
|
+
obj.forme_validations.merge!(h[:validations])
|
133
|
+
end
|
134
|
+
|
135
|
+
if block_given?
|
136
|
+
yield h[:form_version], obj
|
137
|
+
end
|
138
|
+
|
139
|
+
obj
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Raise error with message based on type
|
145
|
+
def _forme_set_handle_error(type, _obj)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Raise error with message based on type
|
149
|
+
def _forme_parse_error(type, obj)
|
150
|
+
_forme_set_handle_error(type, obj)
|
151
|
+
raise Error, ERROR_MESSAGES[type]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Use form class that adds hidden fields for metadata.
|
155
|
+
def _forme_form_class
|
156
|
+
Form
|
157
|
+
end
|
158
|
+
|
159
|
+
# Include a reference to the current scope to the form. This reference is needed
|
160
|
+
# to correctly construct the HMAC.
|
161
|
+
def _forme_form_options(options)
|
162
|
+
options.merge!(:roda=>self)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Internals of forme_parse_hmac and forme_set_hmac.
|
166
|
+
def _forme_parse(obj)
|
167
|
+
params = request.params
|
168
|
+
return _forme_parse_error(:missing_data, obj) unless data = params['_forme_set_data']
|
169
|
+
return _forme_parse_error(:missing_hmac, obj) unless hmac = params['_forme_set_data_hmac']
|
170
|
+
|
171
|
+
data = data.to_s
|
172
|
+
hmac = hmac.to_s
|
173
|
+
actual = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, self.class.opts[:forme_set_hmac_secret], data)
|
174
|
+
unless Rack::Utils.secure_compare(hmac.ljust(64), actual) && hmac.length == actual.length
|
175
|
+
return _forme_parse_error(:hmac_mismatch, obj)
|
176
|
+
end
|
177
|
+
|
178
|
+
data = JSON.parse(data)
|
179
|
+
csrf_field, hmac_csrf_value = data['csrf']
|
180
|
+
if csrf_field
|
181
|
+
csrf_value = params[csrf_field].to_s
|
182
|
+
hmac_csrf_value = hmac_csrf_value.to_s
|
183
|
+
unless Rack::Utils.secure_compare(csrf_value.ljust(hmac_csrf_value.length), hmac_csrf_value) && csrf_value.length == hmac_csrf_value.length
|
184
|
+
return _forme_parse_error(:csrf_mismatch, obj)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
namespaces = data['namespaces']
|
189
|
+
namespaces.each do |key|
|
190
|
+
return _forme_parse_error(:missing_namespace, obj) unless params = params[key]
|
191
|
+
end
|
192
|
+
|
193
|
+
if valid_values = data['valid_values']
|
194
|
+
validations = {}
|
195
|
+
valid_values.each do |col, (type, values)|
|
196
|
+
value = params[col]
|
197
|
+
valid = if type == "subset"
|
198
|
+
!value || (value - values).empty?
|
199
|
+
else # type == "include"
|
200
|
+
values.include?(value)
|
201
|
+
end
|
202
|
+
|
203
|
+
validations[col.to_sym] = [:valid, valid]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
{:params=>params, :columns=>data["columns"], :validations=>validations, :form_version=>data['form_version']}
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
register_plugin(:forme_set, FormeSet)
|
213
|
+
end
|
214
|
+
end
|
data/lib/sequel/plugins/forme.rb
CHANGED
@@ -470,10 +470,10 @@ module Sequel # :nodoc:
|
|
470
470
|
include SequelForm
|
471
471
|
end
|
472
472
|
|
473
|
-
|
474
|
-
|
475
|
-
FORM_CLASSES = {::Forme::Form=>Form}
|
473
|
+
MUTEX = Mutex.new
|
474
|
+
FORM_CLASSES = {::Forme::Form=>Form}
|
476
475
|
|
476
|
+
module InstanceMethods
|
477
477
|
# Configure the +form+ with support for <tt>Sequel::Model</tt>
|
478
478
|
# specific code, such as support for nested attributes.
|
479
479
|
def forme_config(form)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Sequel # :nodoc:
|
4
4
|
module Plugins # :nodoc:
|
5
5
|
# The forme_set plugin makes the model instance keep track of which form
|
6
|
-
# inputs have been added for it. It adds a forme_set method to handle
|
6
|
+
# inputs have been added for it. It adds a <tt>forme_set(params['model_name'])</tt> method to handle
|
7
7
|
# the intake of submitted data from the form. For more complete control,
|
8
8
|
# it also adds a forme_parse method that returns a hash of information that can be
|
9
9
|
# used to modify and validate the object.
|
@@ -47,34 +47,11 @@ module Sequel # :nodoc:
|
|
47
47
|
validations = hash[:validations] = {}
|
48
48
|
|
49
49
|
forme_inputs.each do |field, input|
|
50
|
-
|
51
|
-
next if SKIP_FORMATTERS.include?(opts.fetch(:formatter){input.form_opts[:formatter]})
|
52
|
-
|
53
|
-
if attr = opts[:attr]
|
54
|
-
name = attr[:name] || attr['name']
|
55
|
-
end
|
56
|
-
name ||= opts[:name] || opts[:key] || next
|
57
|
-
|
58
|
-
# Pull out last component of the name if there is one
|
59
|
-
column = (name =~ /\[([^\[\]]+)\]\z/ ? $1 : name)
|
60
|
-
column = column.to_s.sub(/\[\]\z/, '').to_sym
|
61
|
-
|
50
|
+
next unless column = forme_column_for_input(input)
|
62
51
|
hash_values[column] = params[column] || params[column.to_s]
|
63
52
|
|
64
|
-
next unless
|
65
|
-
|
66
|
-
|
67
|
-
values = if opts[:text_method]
|
68
|
-
value_method = opts[:value_method] || opts[:text_method]
|
69
|
-
options.map(&value_method)
|
70
|
-
else
|
71
|
-
options.map{|obj| obj.is_a?(Array) ? obj.last : obj}
|
72
|
-
end
|
73
|
-
|
74
|
-
if ref[:type] == :many_to_one && !opts[:required]
|
75
|
-
values << nil
|
76
|
-
end
|
77
|
-
validations[column] = [ref[:type] != :many_to_one ? :subset : :include, values]
|
53
|
+
next unless validation = forme_validation_for_input(field, input)
|
54
|
+
validations[column] = validation
|
78
55
|
end
|
79
56
|
|
80
57
|
hash
|
@@ -88,6 +65,7 @@ module Sequel # :nodoc:
|
|
88
65
|
unless hash[:validations].empty?
|
89
66
|
forme_validations.merge!(hash[:validations])
|
90
67
|
end
|
68
|
+
nil
|
91
69
|
end
|
92
70
|
|
93
71
|
# Check associated values to ensure they match one of options in the form.
|
@@ -105,6 +83,8 @@ module Sequel # :nodoc:
|
|
105
83
|
!value || (value - values).empty?
|
106
84
|
when :include
|
107
85
|
values.include?(value)
|
86
|
+
when :valid
|
87
|
+
values
|
108
88
|
else
|
109
89
|
raise Forme::Error, "invalid type used in forme_validations"
|
110
90
|
end
|
@@ -115,6 +95,46 @@ module Sequel # :nodoc:
|
|
115
95
|
end
|
116
96
|
end
|
117
97
|
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Return the model column name to use for the given form input.
|
102
|
+
def forme_column_for_input(input)
|
103
|
+
opts = input.opts
|
104
|
+
return if SKIP_FORMATTERS.include?(opts.fetch(:formatter){input.form_opts[:formatter]})
|
105
|
+
|
106
|
+
if attr = opts[:attr]
|
107
|
+
name = attr[:name] || attr['name']
|
108
|
+
end
|
109
|
+
return unless name ||= opts[:name] || opts[:key]
|
110
|
+
|
111
|
+
# Pull out last component of the name if there is one
|
112
|
+
column = name.to_s.chomp('[]')
|
113
|
+
if column =~ /\[([^\[\]]+)\]\z/
|
114
|
+
$1
|
115
|
+
else
|
116
|
+
column
|
117
|
+
end.to_sym
|
118
|
+
end
|
119
|
+
|
120
|
+
# Return the validation metadata to use for the given field name and form input.
|
121
|
+
def forme_validation_for_input(field, input)
|
122
|
+
return unless ref = model.association_reflection(field)
|
123
|
+
opts = input.opts
|
124
|
+
return unless options = opts[:options]
|
125
|
+
|
126
|
+
values = if opts[:text_method]
|
127
|
+
value_method = opts[:value_method] || opts[:text_method]
|
128
|
+
options.map(&value_method)
|
129
|
+
else
|
130
|
+
options.map{|obj| obj.is_a?(Array) ? obj.last : obj}
|
131
|
+
end
|
132
|
+
|
133
|
+
if ref[:type] == :many_to_one && !opts[:required]
|
134
|
+
values << nil
|
135
|
+
end
|
136
|
+
[ref[:type] != :many_to_one ? :subset : :include, values]
|
137
|
+
end
|
118
138
|
end
|
119
139
|
end
|
120
140
|
end
|
@@ -20,6 +20,7 @@ class FormeRails < Rails::Application
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
config.active_support.deprecation = :stderr
|
23
|
+
config.middleware.delete(ActionDispatch::HostAuthorization) if defined?(ActionDispatch::HostAuthorization)
|
23
24
|
config.middleware.delete(ActionDispatch::ShowExceptions)
|
24
25
|
config.middleware.delete(Rack::Lock)
|
25
26
|
config.secret_key_base = 'foo'*15
|
@@ -122,6 +122,354 @@ else
|
|
122
122
|
sin_get('/csrf/0').wont_include '<input name="_csrf" type="hidden" value="'
|
123
123
|
end
|
124
124
|
end
|
125
|
+
|
126
|
+
describe "Forme Roda ERB Sequel integration with roda forme_set plugin and route_csrf plugin with #{plugin_opts}" do
|
127
|
+
before do
|
128
|
+
@app = Class.new(FormeRodaTest)
|
129
|
+
@app.plugin :route_csrf, plugin_opts
|
130
|
+
@app.plugin(:forme_set, :secret=>'1'*64)
|
131
|
+
|
132
|
+
@ab = Album.new
|
133
|
+
end
|
134
|
+
|
135
|
+
def forme_parse(*args, &block)
|
136
|
+
_forme_set(:forme_parse, *args, &block)
|
137
|
+
end
|
138
|
+
|
139
|
+
def forme_set(*args, &block)
|
140
|
+
_forme_set(:forme_set, *args, &block)
|
141
|
+
end
|
142
|
+
|
143
|
+
def forme_call(params)
|
144
|
+
@app.call('REQUEST_METHOD'=>'POST', 'rack.input'=>StringIO.new, :params=>params)
|
145
|
+
end
|
146
|
+
|
147
|
+
def _forme_set(meth, obj, orig_hash, *form_args, &block)
|
148
|
+
hash = {}
|
149
|
+
forme_set_block = orig_hash.delete(:forme_set_block)
|
150
|
+
orig_hash.each{|k,v| hash[k.to_s] = v}
|
151
|
+
album = @ab
|
152
|
+
ret, form, data, hmac = nil
|
153
|
+
|
154
|
+
@app.route do |r|
|
155
|
+
r.get do
|
156
|
+
form(*env[:args], &env[:block]).to_s
|
157
|
+
end
|
158
|
+
r.post do
|
159
|
+
r.params.replace(env[:params])
|
160
|
+
ret = send(meth, album, &forme_set_block)
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
end
|
164
|
+
body = @app.call('REQUEST_METHOD'=>'GET', :args=>[album, *form_args], :block=>block)[2].join
|
165
|
+
body =~ %r|<input name="_csrf" type="hidden" value="([^"]+)"/>.*<input name="_forme_set_data" type="hidden" value="([^"]+)"/><input name="_forme_set_data_hmac" type="hidden" value="([^"]+)"/>|n
|
166
|
+
csrf = $1
|
167
|
+
data = $2
|
168
|
+
hmac = $3
|
169
|
+
data.gsub!(""", '"') if data
|
170
|
+
h = {"album"=>hash, "_forme_set_data"=>data, "_forme_set_data_hmac"=>hmac, "_csrf"=>csrf}
|
171
|
+
if data && hmac
|
172
|
+
forme_call(h)
|
173
|
+
end
|
174
|
+
meth == :forme_parse ? ret : h
|
175
|
+
end
|
176
|
+
|
177
|
+
it "#forme_set should include HMAC values if form includes inputs for obj" do
|
178
|
+
h = forme_set(@ab, :name=>'Foo')
|
179
|
+
proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
|
180
|
+
@ab.name.must_be_nil
|
181
|
+
@ab.copies_sold.must_be_nil
|
182
|
+
|
183
|
+
h = forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
|
184
|
+
hmac = h.delete("_forme_set_data_hmac")
|
185
|
+
proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
|
186
|
+
proc{forme_call(h.merge("_forme_set_data_hmac"=>hmac+'1'))}.must_raise Roda::RodaPlugins::FormeSet::Error
|
187
|
+
data = h["_forme_set_data"]
|
188
|
+
data.sub!(/"csrf":\["_csrf","./, "\"csrf\":[\"_csrf\",\"|")
|
189
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, '1'*64, data)
|
190
|
+
proc{forme_call(h.merge("_forme_set_data_hmac"=>hmac))}.must_raise Roda::RodaPlugins::FormeSet::Error
|
191
|
+
@ab.name.must_equal 'Foo'
|
192
|
+
@ab.copies_sold.must_be_nil
|
193
|
+
|
194
|
+
forme_set(@ab, :copies_sold=>100){|f| f.input(:name)}
|
195
|
+
@ab.name.must_be_nil
|
196
|
+
@ab.copies_sold.must_be_nil
|
197
|
+
end
|
198
|
+
|
199
|
+
it "#forme_set should handle custom form namespaces" do
|
200
|
+
forme_set(@ab, {"album"=>{"name"=>'Foo', 'copies_sold'=>'100'}}, {}, :namespace=>'album'){|f| f.input(:name); f.input(:copies_sold)}
|
201
|
+
@ab.name.must_equal 'Foo'
|
202
|
+
@ab.copies_sold.must_equal 100
|
203
|
+
|
204
|
+
proc{forme_set(@ab, {"a"=>{"name"=>'Foo'}}, {}, :namespace=>'album'){|f| f.input(:name); f.input(:copies_sold)}}.must_raise Roda::RodaPlugins::FormeSet::Error
|
205
|
+
end
|
206
|
+
|
207
|
+
it "#forme_set should call plugin block if there is an error with the form submission hmac not matching data" do
|
208
|
+
@app.plugin :forme_set do |error_type, _|
|
209
|
+
request.on{error_type.to_s}
|
210
|
+
end
|
211
|
+
|
212
|
+
h = forme_set(@ab, :name=>'Foo')
|
213
|
+
forme_call(h)[2].must_equal ['missing_data']
|
214
|
+
|
215
|
+
h = forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
|
216
|
+
hmac = h.delete("_forme_set_data_hmac")
|
217
|
+
forme_call(h)[2].must_equal ['missing_hmac']
|
218
|
+
|
219
|
+
forme_call(h.merge("_forme_set_data_hmac"=>hmac+'1'))[2].must_equal ['hmac_mismatch']
|
220
|
+
|
221
|
+
data = h["_forme_set_data"]
|
222
|
+
data.sub!(/"csrf":\["_csrf","./, "\"csrf\":[\"_csrf\",\"|")
|
223
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, '1'*64, data)
|
224
|
+
forme_call(h.merge("_forme_set_data_hmac"=>hmac))[2].must_equal ['csrf_mismatch']
|
225
|
+
|
226
|
+
h = forme_set(@ab, :name=>'Foo')
|
227
|
+
h.delete('album')
|
228
|
+
forme_call(h)[2].must_equal ['missing_namespace']
|
229
|
+
end
|
230
|
+
|
231
|
+
it "#forme_set should raise if plugin block does not raise or throw" do
|
232
|
+
@app.plugin :forme_set do |_, obj|
|
233
|
+
obj
|
234
|
+
end
|
235
|
+
h = forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
|
236
|
+
h.delete("_forme_set_data_hmac")
|
237
|
+
proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
|
238
|
+
end
|
239
|
+
|
240
|
+
it "#forme_set should only set values in the form" do
|
241
|
+
forme_set(@ab, :name=>'Foo')
|
242
|
+
@ab.name.must_be_nil
|
243
|
+
|
244
|
+
forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
|
245
|
+
@ab.name.must_equal 'Foo'
|
246
|
+
|
247
|
+
forme_set(@ab, 'copies_sold'=>'1'){|f| f.input(:name)}
|
248
|
+
@ab.name.must_be_nil
|
249
|
+
@ab.copies_sold.must_be_nil
|
250
|
+
|
251
|
+
forme_set(@ab, 'name'=>'Bar', 'copies_sold'=>'1'){|f| f.input(:name); f.input(:copies_sold)}
|
252
|
+
@ab.name.must_equal 'Bar'
|
253
|
+
@ab.copies_sold.must_equal 1
|
254
|
+
end
|
255
|
+
|
256
|
+
it "#forme_set should handle form_versions" do
|
257
|
+
h = forme_set(@ab, {:name=>'Foo'}){|f| f.input(:name)}
|
258
|
+
@ab.name.must_equal 'Foo'
|
259
|
+
|
260
|
+
obj = nil
|
261
|
+
version = nil
|
262
|
+
name = nil
|
263
|
+
forme_set_block = proc do |v, o|
|
264
|
+
obj = o
|
265
|
+
name = o.name
|
266
|
+
version = v
|
267
|
+
end
|
268
|
+
h2 = forme_set(@ab, {:name=>'Foo', :forme_set_block=>forme_set_block}, {}, :form_version=>1){|f| f.input(:name)}
|
269
|
+
obj.must_be_same_as @ab
|
270
|
+
name.must_equal 'Foo'
|
271
|
+
version.must_equal 1
|
272
|
+
|
273
|
+
forme_call(h)
|
274
|
+
obj.must_be_same_as @ab
|
275
|
+
version.must_be_nil
|
276
|
+
|
277
|
+
h3 = forme_set(@ab, {:name=>'Bar', :forme_set_block=>forme_set_block}, {}, :form_version=>2){|f| f.input(:name)}
|
278
|
+
obj.must_be_same_as @ab
|
279
|
+
name.must_equal 'Bar'
|
280
|
+
version.must_equal 2
|
281
|
+
|
282
|
+
h['album']['name'] = 'Baz'
|
283
|
+
forme_call(h)
|
284
|
+
obj.must_be_same_as @ab
|
285
|
+
name.must_equal 'Baz'
|
286
|
+
version.must_be_nil
|
287
|
+
|
288
|
+
forme_call(h2)
|
289
|
+
obj.must_be_same_as @ab
|
290
|
+
version.must_equal 1
|
291
|
+
end
|
292
|
+
|
293
|
+
it "#forme_set should work for forms without blocks" do
|
294
|
+
forme_set(@ab, {:name=>'Foo'}, {}, :inputs=>[:name])
|
295
|
+
@ab.name.must_equal 'Foo'
|
296
|
+
end
|
297
|
+
|
298
|
+
it "#forme_set should handle different ways to specify parameter names" do
|
299
|
+
[{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
|
300
|
+
forme_set(@ab, name=>'Foo'){|f| f.input(:name, opts)}
|
301
|
+
@ab.name.must_be_nil
|
302
|
+
|
303
|
+
forme_set(@ab, 'foo'=>'Foo'){|f| f.input(:name, opts)}
|
304
|
+
@ab.name.must_equal 'Foo'
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
it "#forme_set should ignore values where key is explicitly set to nil" do
|
309
|
+
forme_set(@ab, :name=>'Foo'){|f| f.input(:name, :key=>nil)}
|
310
|
+
@ab.forme_set(:name=>'Foo')
|
311
|
+
@ab.name.must_be_nil
|
312
|
+
@ab.forme_set(nil=>'Foo')
|
313
|
+
@ab.name.must_be_nil
|
314
|
+
end
|
315
|
+
|
316
|
+
it "#forme_set should skip inputs with disabled/readonly formatter set on input" do
|
317
|
+
[:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly].each do |formatter|
|
318
|
+
forme_set(@ab, :name=>'Foo'){|f| f.input(:name, :formatter=>formatter)}
|
319
|
+
@ab.name.must_be_nil
|
320
|
+
end
|
321
|
+
|
322
|
+
forme_set(@ab, :name=>'Foo'){|f| f.input(:name, :formatter=>:default)}
|
323
|
+
@ab.name.must_equal 'Foo'
|
324
|
+
end
|
325
|
+
|
326
|
+
it "#forme_set should skip inputs with disabled/readonly formatter set on Form" do
|
327
|
+
[:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly].each do |formatter|
|
328
|
+
forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:disabled){|f| f.input(:name)}
|
329
|
+
@ab.name.must_be_nil
|
330
|
+
end
|
331
|
+
|
332
|
+
forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:default){|f| f.input(:name)}
|
333
|
+
@ab.name.must_equal 'Foo'
|
334
|
+
end
|
335
|
+
|
336
|
+
it "#forme_set should skip inputs with disabled/readonly formatter set using with_opts" do
|
337
|
+
[:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly].each do |formatter|
|
338
|
+
forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>formatter){f.input(:name)}}
|
339
|
+
@ab.name.must_be_nil
|
340
|
+
end
|
341
|
+
|
342
|
+
forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>:default){f.input(:name)}}
|
343
|
+
@ab.name.must_equal 'Foo'
|
344
|
+
end
|
345
|
+
|
346
|
+
it "#forme_set should prefer input formatter to with_opts formatter" do
|
347
|
+
forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>:default){f.input(:name, :formatter=>:readonly)}}
|
348
|
+
@ab.name.must_be_nil
|
349
|
+
|
350
|
+
forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>:readonly){f.input(:name, :formatter=>:default)}}
|
351
|
+
@ab.name.must_equal 'Foo'
|
352
|
+
end
|
353
|
+
|
354
|
+
it "#forme_set should prefer with_opts formatter to form formatter" do
|
355
|
+
forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:default){|f| f.with_opts(:formatter=>:readonly){f.input(:name)}}
|
356
|
+
@ab.name.must_be_nil
|
357
|
+
|
358
|
+
forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:readonly){|f| f.with_opts(:formatter=>:default){f.input(:name)}}
|
359
|
+
@ab.name.must_equal 'Foo'
|
360
|
+
end
|
361
|
+
|
362
|
+
it "#forme_set should handle setting values for associated objects" do
|
363
|
+
forme_set(@ab, :artist_id=>'1')
|
364
|
+
@ab.artist_id.must_be_nil
|
365
|
+
|
366
|
+
forme_set(@ab, :artist_id=>'1'){|f| f.input(:artist)}
|
367
|
+
@ab.artist_id.must_equal 1
|
368
|
+
|
369
|
+
forme_set(@ab, 'tag_pks'=>%w'1 2'){|f| f.input(:artist)}
|
370
|
+
@ab.artist_id.must_be_nil
|
371
|
+
@ab.tag_pks.must_equal []
|
372
|
+
|
373
|
+
forme_set(@ab, 'artist_id'=>'1', 'tag_pks'=>%w'1 2'){|f| f.input(:artist); f.input(:tags)}
|
374
|
+
@ab.artist_id.must_equal 1
|
375
|
+
@ab.tag_pks.must_equal [1, 2]
|
376
|
+
end
|
377
|
+
|
378
|
+
it "#forme_set should handle validations for filtered associations" do
|
379
|
+
[
|
380
|
+
[{:dataset=>proc{|ds| ds.exclude(:id=>1)}},
|
381
|
+
{:dataset=>proc{|ds| ds.exclude(:id=>1)}}],
|
382
|
+
[{:options=>Artist.exclude(:id=>1).select_order_map([:name, :id])},
|
383
|
+
{:options=>Tag.exclude(:id=>1).select_order_map(:id), :name=>'tag_pks[]'}],
|
384
|
+
[{:options=>Artist.exclude(:id=>1).all, :text_method=>:name, :value_method=>:id},
|
385
|
+
{:options=>Tag.exclude(:id=>1).all, :text_method=>:name, :value_method=>:id}],
|
386
|
+
].each do |artist_opts, tag_opts|
|
387
|
+
@ab.forme_validations.clear
|
388
|
+
forme_set(@ab, 'artist_id'=>'1', 'tag_pks'=>%w'1 2'){|f| f.input(:artist, artist_opts); f.input(:tags, tag_opts)}
|
389
|
+
@ab.artist_id.must_equal 1
|
390
|
+
@ab.tag_pks.must_equal [1, 2]
|
391
|
+
@ab.valid?.must_equal false
|
392
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
393
|
+
@ab.errors[:tag_pks].must_equal ['invalid value submitted']
|
394
|
+
|
395
|
+
@ab.forme_validations.clear
|
396
|
+
forme_set(@ab, 'artist_id'=>'1', 'tag_pks'=>%w'2'){|f| f.input(:artist, artist_opts); f.input(:tags, tag_opts)}
|
397
|
+
@ab.forme_set('artist_id'=>'1', 'tag_pks'=>['2'])
|
398
|
+
@ab.artist_id.must_equal 1
|
399
|
+
@ab.tag_pks.must_equal [2]
|
400
|
+
@ab.valid?.must_equal false
|
401
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
402
|
+
@ab.errors[:tag_pks].must_be_nil
|
403
|
+
|
404
|
+
@ab.forme_validations.clear
|
405
|
+
forme_set(@ab, 'artist_id'=>'2', 'tag_pks'=>%w'2'){|f| f.input(:artist, artist_opts); f.input(:tags, tag_opts)}
|
406
|
+
@ab.valid?.must_equal true
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
it "#forme_set should not require associated values for many_to_one association with select boxes" do
|
411
|
+
forme_set(@ab, {}){|f| f.input(:artist)}
|
412
|
+
@ab.valid?.must_equal true
|
413
|
+
|
414
|
+
forme_set(@ab, {'artist_id'=>nil}){|f| f.input(:artist)}
|
415
|
+
@ab.valid?.must_equal true
|
416
|
+
|
417
|
+
forme_set(@ab, {'artist_id'=>''}){|f| f.input(:artist)}
|
418
|
+
@ab.valid?.must_equal true
|
419
|
+
end
|
420
|
+
|
421
|
+
it "#forme_set should not require associated values for many_to_one association with radio buttons" do
|
422
|
+
forme_set(@ab, {}){|f| f.input(:artist, :as=>:radio)}
|
423
|
+
@ab.valid?.must_equal true
|
424
|
+
end
|
425
|
+
|
426
|
+
it "#forme_set should require associated values for many_to_one association with select boxes when :required is used" do
|
427
|
+
forme_set(@ab, {}){|f| f.input(:artist, :required=>true)}
|
428
|
+
@ab.valid?.must_equal false
|
429
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
430
|
+
end
|
431
|
+
|
432
|
+
it "#forme_set should require associated values for many_to_one association with radio buttons when :required is used" do
|
433
|
+
forme_set(@ab, {}){|f| f.input(:artist, :as=>:radio, :required=>true)}
|
434
|
+
@ab.valid?.must_equal false
|
435
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
436
|
+
end
|
437
|
+
|
438
|
+
it "#forme_set should handle cases where currently associated values is nil" do
|
439
|
+
def @ab.tag_pks; nil; end
|
440
|
+
forme_set(@ab, :tag_pks=>['1']){|f| f.input(:tags)}
|
441
|
+
@ab.valid?.must_equal true
|
442
|
+
end
|
443
|
+
|
444
|
+
it "#forme_parse should return hash with values and validations" do
|
445
|
+
forme_parse(@ab, :name=>'Foo'){|f| f.input(:name)}.must_equal(:values=>{:name=>'Foo'}, :validations=>{}, :form_version=>nil)
|
446
|
+
|
447
|
+
hash = forme_parse(@ab, :name=>'Foo', 'artist_id'=>'1') do |f|
|
448
|
+
f.input(:name)
|
449
|
+
f.input(:artist, :dataset=>proc{|ds| ds.exclude(:id=>1)})
|
450
|
+
end
|
451
|
+
hash.must_equal(:values=>{:name=>'Foo', :artist_id=>'1'}, :validations=>{:artist_id=>[:valid, false]}, :form_version=>nil)
|
452
|
+
|
453
|
+
@ab.set(hash[:values])
|
454
|
+
@ab.valid?.must_equal true
|
455
|
+
|
456
|
+
@ab.forme_validations.merge!(hash[:validations])
|
457
|
+
@ab.valid?.must_equal false
|
458
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
459
|
+
|
460
|
+
@ab = Album.new
|
461
|
+
hash = forme_parse(@ab, {:name=>'Foo', 'artist_id'=>'1'}, {}, :form_version=>1) do |f|
|
462
|
+
f.input(:name)
|
463
|
+
f.input(:artist, :dataset=>proc{|ds| ds.exclude(:id=>2)})
|
464
|
+
end
|
465
|
+
hash.must_equal(:values=>{:name=>'Foo', :artist_id=>'1'}, :validations=>{:artist_id=>[:valid, true]}, :form_version=>1)
|
466
|
+
@ab.set(hash[:values])
|
467
|
+
@ab.valid?.must_equal true
|
468
|
+
|
469
|
+
@ab.forme_validations.merge!(hash[:validations])
|
470
|
+
@ab.valid?.must_equal true
|
471
|
+
end
|
472
|
+
end
|
125
473
|
end
|
126
474
|
end
|
127
475
|
end
|
@@ -26,7 +26,7 @@ describe "Sequel forme_set plugin" do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it "#forme_set should handle different ways to specify parameter names" do
|
29
|
-
[{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
|
29
|
+
[{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'foo[]'}, {:name=>'bar[foo][]'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
|
30
30
|
@f.input(:name, opts)
|
31
31
|
|
32
32
|
@ab.forme_set(:name=>'Foo')
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forme
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 5.7.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest-global_expectations
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: sequel
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,7 +81,7 @@ dependencies:
|
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
84
|
+
name: erubi
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
87
|
- - ">="
|
@@ -175,6 +189,7 @@ files:
|
|
175
189
|
- lib/forme/version.rb
|
176
190
|
- lib/roda/plugins/forme.rb
|
177
191
|
- lib/roda/plugins/forme_route_csrf.rb
|
192
|
+
- lib/roda/plugins/forme_set.rb
|
178
193
|
- lib/sequel/plugins/forme.rb
|
179
194
|
- lib/sequel/plugins/forme_i18n.rb
|
180
195
|
- lib/sequel/plugins/forme_set.rb
|
@@ -197,7 +212,12 @@ files:
|
|
197
212
|
homepage: http://github.com/jeremyevans/forme
|
198
213
|
licenses:
|
199
214
|
- MIT
|
200
|
-
metadata:
|
215
|
+
metadata:
|
216
|
+
bug_tracker_uri: https://github.com/jeremyevans/forme/issues
|
217
|
+
changelog_uri: http://forme.jeremyevans.net/files/CHANGELOG.html
|
218
|
+
documentation_uri: http://forme.jeremyevans.net
|
219
|
+
mailing_list_uri: https://groups.google.com/forum/#!forum/ruby-forme
|
220
|
+
source_code_uri: https://github.com/jeremyevans/forme
|
201
221
|
post_install_message:
|
202
222
|
rdoc_options:
|
203
223
|
- "--quiet"
|
@@ -220,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
240
|
- !ruby/object:Gem::Version
|
221
241
|
version: '0'
|
222
242
|
requirements: []
|
223
|
-
rubygems_version: 3.
|
243
|
+
rubygems_version: 3.1.2
|
224
244
|
signing_key:
|
225
245
|
specification_version: 4
|
226
246
|
summary: HTML forms library
|