forme 1.10.0 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|