form_input 0.9.0.pre1
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 +7 -0
- data/.gitignore +5 -0
- data/LICENSE +19 -0
- data/README.md +3160 -0
- data/Rakefile +19 -0
- data/example/controllers/ramaze/press_release.rb +104 -0
- data/example/controllers/ramaze/profile.rb +38 -0
- data/example/controllers/sinatra/press_release.rb +114 -0
- data/example/controllers/sinatra/profile.rb +39 -0
- data/example/forms/change_password_form.rb +17 -0
- data/example/forms/login_form.rb +14 -0
- data/example/forms/lost_password_form.rb +14 -0
- data/example/forms/new_password_form.rb +15 -0
- data/example/forms/password_form.rb +18 -0
- data/example/forms/press_release_form.rb +153 -0
- data/example/forms/profile_form.rb +21 -0
- data/example/forms/signup_form.rb +25 -0
- data/example/views/press_release.slim +65 -0
- data/example/views/profile.slim +28 -0
- data/example/views/snippets/form_block.slim +27 -0
- data/example/views/snippets/form_chunked.slim +25 -0
- data/example/views/snippets/form_hidden.slim +21 -0
- data/example/views/snippets/form_panel.slim +89 -0
- data/form_input.gemspec +32 -0
- data/lib/form_input/core.rb +1165 -0
- data/lib/form_input/localize.rb +49 -0
- data/lib/form_input/r18n/cs.yml +97 -0
- data/lib/form_input/r18n/en.yml +70 -0
- data/lib/form_input/r18n/pl.yml +122 -0
- data/lib/form_input/r18n/sk.yml +120 -0
- data/lib/form_input/r18n.rb +163 -0
- data/lib/form_input/steps.rb +365 -0
- data/lib/form_input/types.rb +176 -0
- data/lib/form_input/version.rb +12 -0
- data/lib/form_input.rb +5 -0
- data/test/helper.rb +21 -0
- data/test/localize/en.yml +63 -0
- data/test/r18n/cs.yml +60 -0
- data/test/r18n/xx.yml +51 -0
- data/test/reference/cs.txt +352 -0
- data/test/reference/cs.yml +14 -0
- data/test/reference/en.txt +76 -0
- data/test/reference/en.yml +8 -0
- data/test/reference/pl.txt +440 -0
- data/test/reference/pl.yml +16 -0
- data/test/reference/sk.txt +352 -0
- data/test/reference/sk.yml +14 -0
- data/test/test_core.rb +1272 -0
- data/test/test_localize.rb +27 -0
- data/test/test_r18n.rb +373 -0
- data/test/test_steps.rb +482 -0
- data/test/test_types.rb +307 -0
- metadata +145 -0
@@ -0,0 +1,365 @@
|
|
1
|
+
# Support for multi-step forms.
|
2
|
+
|
3
|
+
class FormInput
|
4
|
+
|
5
|
+
# Turn this form into multi-step form using given steps.
|
6
|
+
# Returns self for chaining.
|
7
|
+
def self.define_steps( steps )
|
8
|
+
@steps = steps = steps.to_hash.dup.freeze
|
9
|
+
|
10
|
+
self.send( :include, StepMethods )
|
11
|
+
|
12
|
+
opts = { filter: ->{ steps.keys.find{ |x| x.to_s == self } }, class: Symbol }
|
13
|
+
|
14
|
+
param :step, opts, type: :hidden
|
15
|
+
param :next, opts, type: :ignore
|
16
|
+
param :last, opts, type: :hidden
|
17
|
+
param :seen, opts, type: :hidden
|
18
|
+
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get hash mapping defined steps to their names, or nil if there are none.
|
23
|
+
def self.form_steps
|
24
|
+
@steps
|
25
|
+
end
|
26
|
+
|
27
|
+
# Additional methods used for multi-step form processing.
|
28
|
+
module StepMethods
|
29
|
+
|
30
|
+
# Initialize new instance.
|
31
|
+
def initialize( *args )
|
32
|
+
super
|
33
|
+
self.seen = last_step( seen, step )
|
34
|
+
self.step ||= steps.first
|
35
|
+
self.next ||= step
|
36
|
+
self.last ||= step
|
37
|
+
if correct_step?
|
38
|
+
self.step = self.next
|
39
|
+
self.seen = last_step( seen, previous_step( step ) )
|
40
|
+
end
|
41
|
+
self.last = last_step( step, last )
|
42
|
+
end
|
43
|
+
|
44
|
+
# Make all steps instantly available.
|
45
|
+
# Returns self for chaining.
|
46
|
+
def unlock_steps
|
47
|
+
self.last = self.seen = steps.last
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get parameters relevant for given step.
|
52
|
+
def step_params( step )
|
53
|
+
fail( ArgumentError, "invalid step name #{step}" ) unless form_steps.key?( step )
|
54
|
+
tagged_params( step )
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the parameters relevant for the current step.
|
58
|
+
def current_params
|
59
|
+
tagged_params( step )
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get the parameters irrelevant for the current step.
|
63
|
+
def other_params
|
64
|
+
untagged_params( step )
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get hash mapping defined steps to their names.
|
68
|
+
# Note that this is never localized. See step_names instead.
|
69
|
+
def form_steps
|
70
|
+
self.class.form_steps
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get allowed form steps as list of symbols.
|
74
|
+
def steps
|
75
|
+
form_steps.keys
|
76
|
+
end
|
77
|
+
|
78
|
+
# Get name of current or given step, if any.
|
79
|
+
def step_name( step = self.step )
|
80
|
+
form_steps[ step ]
|
81
|
+
end
|
82
|
+
alias raw_step_name step_name
|
83
|
+
|
84
|
+
# Get hash of steps along with their names, for use in a sidebar.
|
85
|
+
def step_names
|
86
|
+
form_steps.reject{ |k,v| v.nil? }
|
87
|
+
end
|
88
|
+
alias raw_step_names step_names
|
89
|
+
|
90
|
+
# Get index of given/current step among all steps.
|
91
|
+
def step_index( step = self.step )
|
92
|
+
steps.index( step ) or fail( ArgumentError, "invalid step name #{step}" )
|
93
|
+
end
|
94
|
+
|
95
|
+
# Test if current step is before given step.
|
96
|
+
def step_before?( step )
|
97
|
+
step_index < step_index( step )
|
98
|
+
end
|
99
|
+
|
100
|
+
# Test if current step is after given step.
|
101
|
+
def step_after?( step )
|
102
|
+
step_index > step_index( step )
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get first step, or first step among given list of steps, if any.
|
106
|
+
def first_step( *args )
|
107
|
+
if args.empty?
|
108
|
+
steps.first
|
109
|
+
else
|
110
|
+
args.flatten.compact.min_by{ |x| step_index( x ) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get last step, or last step among given list of steps, if any.
|
115
|
+
def last_step( *args )
|
116
|
+
if args.empty?
|
117
|
+
steps.last
|
118
|
+
else
|
119
|
+
args.flatten.compact.max_by{ |x| step_index( x ) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Test if given/current step is the first step.
|
124
|
+
def first_step?( step = self.step )
|
125
|
+
step == first_step
|
126
|
+
end
|
127
|
+
|
128
|
+
# Test if given/current step is the last step.
|
129
|
+
def last_step?( step = self.step )
|
130
|
+
step == last_step
|
131
|
+
end
|
132
|
+
|
133
|
+
# Get steps before given/current step.
|
134
|
+
def previous_steps( step = self.step )
|
135
|
+
index = steps.index( step ) || 0
|
136
|
+
steps.first( index )
|
137
|
+
end
|
138
|
+
|
139
|
+
# Get steps after given/current step.
|
140
|
+
def next_steps( step = self.step )
|
141
|
+
index = steps.index( step ) || -1
|
142
|
+
steps[ index + 1 .. -1 ]
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get the next step, or nil if there is none.
|
146
|
+
def next_step( step = self.step )
|
147
|
+
next_steps( step ).first
|
148
|
+
end
|
149
|
+
|
150
|
+
# Get the previous step, or nil if there is none.
|
151
|
+
def previous_step( step = self.step )
|
152
|
+
previous_steps( step ).last
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get name of the next step, if any.
|
156
|
+
def next_step_name
|
157
|
+
step_name( next_step )
|
158
|
+
end
|
159
|
+
|
160
|
+
# Get name of the previous step, if any.
|
161
|
+
def previous_step_name
|
162
|
+
step_name( previous_step )
|
163
|
+
end
|
164
|
+
|
165
|
+
# Test if the current/given step has no parameters defined.
|
166
|
+
def extra_step?( step = self.step )
|
167
|
+
step_params( step ).empty?
|
168
|
+
end
|
169
|
+
|
170
|
+
# Test if the current/given step has some parameters defined.
|
171
|
+
def regular_step?( step = self.step )
|
172
|
+
not extra_step?( step )
|
173
|
+
end
|
174
|
+
|
175
|
+
# Get steps with no parameters defined.
|
176
|
+
def extra_steps
|
177
|
+
steps.select{ |step| extra_step?( step ) }
|
178
|
+
end
|
179
|
+
|
180
|
+
# Get steps with some parameters defined.
|
181
|
+
def regular_steps
|
182
|
+
steps.select{ |step| regular_step?( step ) }
|
183
|
+
end
|
184
|
+
|
185
|
+
# Filter steps by testing their corresponding parameters with given block. Excludes steps without parameters.
|
186
|
+
def filter_steps
|
187
|
+
steps.select do |step|
|
188
|
+
params = step_params( step )
|
189
|
+
yield params unless params.empty?
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Get steps which have required parameters. Excludes steps without parameters.
|
194
|
+
def required_steps
|
195
|
+
filter_steps{ |params| params.any?{ |p| p.required? } }
|
196
|
+
end
|
197
|
+
|
198
|
+
# Get steps which have no required parameters. Excludes steps without parameters.
|
199
|
+
def optional_steps
|
200
|
+
filter_steps{ |params| params.none?{ |p| p.required? } }
|
201
|
+
end
|
202
|
+
|
203
|
+
# Test if given/current has some required parameters. Considered false for steps without parameters.
|
204
|
+
def required_step?( step = self.step )
|
205
|
+
step_params( step ).any?{ |p| p.required? }
|
206
|
+
end
|
207
|
+
|
208
|
+
# Test if given/current step has no required parameters. Considered true for steps without parameters.
|
209
|
+
def optional_step?( step = self.step )
|
210
|
+
not required_step?( step )
|
211
|
+
end
|
212
|
+
|
213
|
+
# Get steps which have some data filled in. Excludes steps without parameters.
|
214
|
+
def filled_steps
|
215
|
+
filter_steps{ |params| params.any?{ |p| p.filled? } }
|
216
|
+
end
|
217
|
+
|
218
|
+
# Get steps which have no data filled in. Excludes steps without parameters.
|
219
|
+
def unfilled_steps
|
220
|
+
filter_steps{ |params| params.none?{ |p| p.filled? } }
|
221
|
+
end
|
222
|
+
|
223
|
+
# Test if given/current step has some data filled in. Considered true for steps without parameters.
|
224
|
+
def filled_step?( step = self.step )
|
225
|
+
params = step_params( step )
|
226
|
+
params.empty? or params.any?{ |p| p.filled? }
|
227
|
+
end
|
228
|
+
|
229
|
+
# Test if given/current step has no data filled in. Considered false for steps without parameters.
|
230
|
+
def unfilled_step?( step = self.step )
|
231
|
+
not filled_step?( step )
|
232
|
+
end
|
233
|
+
|
234
|
+
# Get steps which have all data filled in correctly. Excludes steps without parameters.
|
235
|
+
def correct_steps
|
236
|
+
filter_steps{ |params| valid?( params ) }
|
237
|
+
end
|
238
|
+
|
239
|
+
# Get steps which have some invalid data filled in. Excludes steps without parameters.
|
240
|
+
def incorrect_steps
|
241
|
+
filter_steps{ |params| invalid?( params ) }
|
242
|
+
end
|
243
|
+
|
244
|
+
# Get first step with invalid data, or nil if there is none.
|
245
|
+
def incorrect_step
|
246
|
+
incorrect_steps.first
|
247
|
+
end
|
248
|
+
|
249
|
+
# Test if the current/given step has all data filled in correctly. Considered true for steps without parameters.
|
250
|
+
def correct_step?( step = self.step )
|
251
|
+
valid?( step_params( step ) )
|
252
|
+
end
|
253
|
+
|
254
|
+
# Test if the current/given step has some invalid data filled in. Considered false for steps without parameters.
|
255
|
+
def incorrect_step?( step = self.step )
|
256
|
+
invalid?( step_params( step ) )
|
257
|
+
end
|
258
|
+
|
259
|
+
# Get steps with some parameters enabled. Excludes steps without parameters.
|
260
|
+
def enabled_steps
|
261
|
+
filter_steps{ |params| params.any?{ |p| p.enabled? } }
|
262
|
+
end
|
263
|
+
|
264
|
+
# Get steps with all parameters disabled. Excludes steps without parameters.
|
265
|
+
def disabled_steps
|
266
|
+
filter_steps{ |params| params.all?{ |p| p.disabled? } }
|
267
|
+
end
|
268
|
+
|
269
|
+
# Test if given/current step has some parameters enabled. Considered true for steps without parameters.
|
270
|
+
def enabled_step?( step = self.step )
|
271
|
+
params = step_params( step )
|
272
|
+
params.empty? or params.any?{ |p| p.enabled? }
|
273
|
+
end
|
274
|
+
|
275
|
+
# Test if given/current step has all parameters disabled. Considered false for steps without parameters.
|
276
|
+
def disabled_step?( step = self.step )
|
277
|
+
not enabled_step?( step )
|
278
|
+
end
|
279
|
+
|
280
|
+
# Get unfinished steps, those we have not yet visited or visited for the first time.
|
281
|
+
def unfinished_steps
|
282
|
+
next_steps( seen )
|
283
|
+
end
|
284
|
+
|
285
|
+
# Get finished steps, those we have visited or skipped over before.
|
286
|
+
def finished_steps
|
287
|
+
steps - unfinished_steps
|
288
|
+
end
|
289
|
+
|
290
|
+
# Test if given/current step was not yet visited or was visited for the first time.
|
291
|
+
def unfinished_step?( step = self.step )
|
292
|
+
index = seen ? step_index( seen ) : -1
|
293
|
+
step_index( step ) > index
|
294
|
+
end
|
295
|
+
|
296
|
+
# Test if given/current step was visited or skipped over before.
|
297
|
+
def finished_step?( step = self.step )
|
298
|
+
not unfinished_step?( step )
|
299
|
+
end
|
300
|
+
|
301
|
+
# Get yet inaccessible steps, excluding the last accessed step.
|
302
|
+
def inaccessible_steps
|
303
|
+
next_steps( last )
|
304
|
+
end
|
305
|
+
|
306
|
+
# Get already accessible steps, including the last accessed step.
|
307
|
+
def accessible_steps
|
308
|
+
steps - inaccessible_steps
|
309
|
+
end
|
310
|
+
|
311
|
+
# Test if given/current step is inaccessible.
|
312
|
+
def inaccessible_step?( step = self.step )
|
313
|
+
step_index( step ) > step_index( last )
|
314
|
+
end
|
315
|
+
|
316
|
+
# Test if given/current step is accessible.
|
317
|
+
def accessible_step?( step = self.step )
|
318
|
+
not inaccessible_step?( step )
|
319
|
+
end
|
320
|
+
|
321
|
+
# Get correct finished steps. Includes finished steps without parameters.
|
322
|
+
def complete_steps
|
323
|
+
finished_steps.select{ |step| correct_step?( step ) }
|
324
|
+
end
|
325
|
+
|
326
|
+
# Get incorrect finished steps. Excludes steps without parameters.
|
327
|
+
def incomplete_steps
|
328
|
+
finished_steps.select{ |step| incorrect_step?( step ) }
|
329
|
+
end
|
330
|
+
|
331
|
+
# Test if given/current step is one of the complete steps.
|
332
|
+
def complete_step?( step = self.step )
|
333
|
+
finished_step?( step ) and correct_step?( step )
|
334
|
+
end
|
335
|
+
|
336
|
+
# Test if given/current step is one of the incomplete steps.
|
337
|
+
def incomplete_step?( step = self.step )
|
338
|
+
finished_step?( step ) and incorrect_step?( step )
|
339
|
+
end
|
340
|
+
|
341
|
+
# Get steps which shell be displayed as correct.
|
342
|
+
def good_steps
|
343
|
+
steps.select{ |step| good_step?( step ) }
|
344
|
+
end
|
345
|
+
|
346
|
+
# Get steps which shell be displayed as incorrect.
|
347
|
+
def bad_steps
|
348
|
+
steps.select{ |step| bad_step?( step ) }
|
349
|
+
end
|
350
|
+
|
351
|
+
# Test if given/current step shell be displayed as correct.
|
352
|
+
def good_step?( step = self.step )
|
353
|
+
complete_step?( step ) and filled_step?( step ) and regular_step?( step )
|
354
|
+
end
|
355
|
+
|
356
|
+
# Test if given/current step shell be displayed as incorrect.
|
357
|
+
def bad_step?( step = self.step )
|
358
|
+
incomplete_step?( step )
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|
364
|
+
|
365
|
+
# EOF #
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# Common form types.
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
# Extend forms with arguments for form parameter types which are used often.
|
7
|
+
class FormInput
|
8
|
+
|
9
|
+
# Regular expressions commonly used to validate input arguments.
|
10
|
+
|
11
|
+
# Matches names using latin alphabet.
|
12
|
+
LATIN_NAMES_RE = /\A[\p{Latin}\-\. ]+\z/u
|
13
|
+
|
14
|
+
# Matches common email addresses. Note that it doesn't match all addresses allowed by RFC, though.
|
15
|
+
SIMPLE_EMAIL_RE = /\A[-_.=+%a-z0-9]+@(?:[-_a-z0-9]+\.)+[a-z]{2,4}\z/i
|
16
|
+
|
17
|
+
# Matches generic ZIP code. Note that the real format changes for each country.
|
18
|
+
ZIP_CODE_RE = /\A[A-Z\d]++(?:[- ]?[A-Z\d]+)*+\z/i
|
19
|
+
|
20
|
+
# Filter for phone numbers.
|
21
|
+
PHONE_NUMBER_FILTER = ->{ gsub( /\s*[-\/\.]\s*/, '-' ).gsub( /\s+/, ' ' ).strip }
|
22
|
+
|
23
|
+
# Matches generic phone number.
|
24
|
+
PHONE_NUMBER_RE = /\A\+?\d++(?:[- ]?(?:\d+|\(\d+\)))*+(?:[- ]?[A-Z\d]+)*+\z/i
|
25
|
+
|
26
|
+
# Basic types.
|
27
|
+
|
28
|
+
# Integer number.
|
29
|
+
INTEGER_ARGS = {
|
30
|
+
filter: ->{ ( Integer( self, 10 ) rescue self ) unless empty? },
|
31
|
+
class: Integer,
|
32
|
+
}
|
33
|
+
|
34
|
+
# Float number.
|
35
|
+
FLOAT_ARGS = {
|
36
|
+
filter: ->{ ( Float( self ) rescue self ) unless empty? },
|
37
|
+
class: Float,
|
38
|
+
}
|
39
|
+
|
40
|
+
# Boolean value, displayed as a select menu.
|
41
|
+
BOOL_ARGS = {
|
42
|
+
type: :select,
|
43
|
+
data: [ [ true, 'Yes' ], [ false, 'No' ] ],
|
44
|
+
filter: ->{ self == 'true' unless empty? },
|
45
|
+
class: [ TrueClass, FalseClass ],
|
46
|
+
}
|
47
|
+
|
48
|
+
# Boolean value, displayed as a checkbox.
|
49
|
+
CHECKBOX_ARGS = {
|
50
|
+
type: :checkbox,
|
51
|
+
filter: ->{ not empty? },
|
52
|
+
format: ->{ self if self },
|
53
|
+
class: [ TrueClass, FalseClass ],
|
54
|
+
}
|
55
|
+
|
56
|
+
# Address fields.
|
57
|
+
|
58
|
+
# Email.
|
59
|
+
EMAIL_ARGS = {
|
60
|
+
match: SIMPLE_EMAIL_RE,
|
61
|
+
}
|
62
|
+
|
63
|
+
# Zip code.
|
64
|
+
ZIP_ARGS = {
|
65
|
+
match: ZIP_CODE_RE,
|
66
|
+
}
|
67
|
+
|
68
|
+
# Phone number.
|
69
|
+
PHONE_ARGS = {
|
70
|
+
filter: PHONE_NUMBER_FILTER,
|
71
|
+
match: PHONE_NUMBER_RE,
|
72
|
+
}
|
73
|
+
|
74
|
+
# Date and time.
|
75
|
+
|
76
|
+
# Full time format.
|
77
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M:%S".freeze
|
78
|
+
# Full time format example.
|
79
|
+
TIME_FORMAT_EXAMPLE = "YYYY-MM-DD HH:MM:SS".freeze
|
80
|
+
|
81
|
+
# Full time.
|
82
|
+
TIME_ARGS = {
|
83
|
+
placeholder: TIME_FORMAT_EXAMPLE,
|
84
|
+
filter: ->{ ( FormInput.parse_time!( self, TIME_FORMAT ) rescue self ) unless empty? },
|
85
|
+
format: ->{ utc.strftime( TIME_FORMAT ) rescue self },
|
86
|
+
class: Time,
|
87
|
+
}
|
88
|
+
|
89
|
+
# US date format.
|
90
|
+
US_DATE_FORMAT = "%m/%d/%Y".freeze
|
91
|
+
# US date format example.
|
92
|
+
US_DATE_FORMAT_EXAMPLE = "MM/DD/YYYY".freeze
|
93
|
+
|
94
|
+
# Time in US date format.
|
95
|
+
US_DATE_ARGS = {
|
96
|
+
placeholder: US_DATE_FORMAT_EXAMPLE,
|
97
|
+
filter: ->{ ( FormInput.parse_time!( self, US_DATE_FORMAT ) rescue self ) unless empty? },
|
98
|
+
format: ->{ utc.strftime( US_DATE_FORMAT ) rescue self },
|
99
|
+
class: Time,
|
100
|
+
}
|
101
|
+
|
102
|
+
# UK date format.
|
103
|
+
UK_DATE_FORMAT = "%d/%m/%Y".freeze
|
104
|
+
# UK date format example.
|
105
|
+
UK_DATE_FORMAT_EXAMPLE = "DD/MM/YYYY".freeze
|
106
|
+
|
107
|
+
# Time in UK date format.
|
108
|
+
UK_DATE_ARGS = {
|
109
|
+
placeholder: UK_DATE_FORMAT_EXAMPLE,
|
110
|
+
filter: ->{ ( FormInput.parse_time!( self, UK_DATE_FORMAT ) rescue self ) unless empty? },
|
111
|
+
format: ->{ utc.strftime( UK_DATE_FORMAT ) rescue self },
|
112
|
+
class: Time,
|
113
|
+
}
|
114
|
+
|
115
|
+
# EU date format.
|
116
|
+
EU_DATE_FORMAT = "%-d.%-m.%Y".freeze
|
117
|
+
# EU date format example.
|
118
|
+
EU_DATE_FORMAT_EXAMPLE = "D.M.YYYY".freeze
|
119
|
+
|
120
|
+
# Time in EU date format.
|
121
|
+
EU_DATE_ARGS = {
|
122
|
+
placeholder: EU_DATE_FORMAT_EXAMPLE,
|
123
|
+
filter: ->{ ( FormInput.parse_time!( self, EU_DATE_FORMAT ) rescue self ) unless empty? },
|
124
|
+
format: ->{ utc.strftime( EU_DATE_FORMAT ) rescue self },
|
125
|
+
class: Time,
|
126
|
+
}
|
127
|
+
|
128
|
+
# Hours format.
|
129
|
+
HOURS_FORMAT = "%H:%M".freeze
|
130
|
+
# Hours format example.
|
131
|
+
HOURS_FORMAT_EXAMPLE = "HH:MM".freeze
|
132
|
+
|
133
|
+
# Seconds since midnight in hours:minutes format.
|
134
|
+
HOURS_ARGS = {
|
135
|
+
placeholder: HOURS_FORMAT_EXAMPLE,
|
136
|
+
filter: ->{ ( FormInput.parse_time( self, HOURS_FORMAT ).to_i % 86400 rescue self ) unless empty? },
|
137
|
+
format: ->{ Time.at( self ).utc.strftime( HOURS_FORMAT ) rescue self },
|
138
|
+
class: Integer,
|
139
|
+
}
|
140
|
+
|
141
|
+
# Parse time like Time#strptime but raise on trailing garbage.
|
142
|
+
# Also ignores -, _ and ^ % modifiers, so the same format can be used for both parsing and formatting.
|
143
|
+
def self.parse_time( string, format )
|
144
|
+
format = format.gsub( /%[-_^]?(.)/, '%\1' )
|
145
|
+
# Rather than using _strptime and checking the leftover field,
|
146
|
+
# add required trailing character to both the string and format parameters.
|
147
|
+
suffix = ( string[ -1 ] == "\1" ? "\2" : "\1" )
|
148
|
+
Time.strptime( "+0000 #{string}#{suffix}", "%z #{format}#{suffix}" ).utc
|
149
|
+
end
|
150
|
+
|
151
|
+
# Like parse_time, but falls back to DateTime.parse heuristics when the date/time can't be parsed.
|
152
|
+
def self.parse_time!( string, format )
|
153
|
+
parse_time( string, format )
|
154
|
+
rescue
|
155
|
+
DateTime.parse( string ).to_time.utc
|
156
|
+
end
|
157
|
+
|
158
|
+
# Transformation which drops empty values from hashes and arrays and turns empty string into nil.
|
159
|
+
PRUNED_ARGS = {
|
160
|
+
transform: ->{
|
161
|
+
case self
|
162
|
+
when Array
|
163
|
+
reject{ |v| v.nil? or ( v.respond_to?( :empty? ) && v.empty? ) }
|
164
|
+
when Hash
|
165
|
+
reject{ |k,v| v.nil? or ( v.respond_to?( :empty? ) && v.empty? ) }
|
166
|
+
when String
|
167
|
+
self unless empty?
|
168
|
+
else
|
169
|
+
self
|
170
|
+
end
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
# EOF #
|
data/lib/form_input.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Test helper.
|
2
|
+
|
3
|
+
# Test coverage if enabled.
|
4
|
+
|
5
|
+
def jruby?
|
6
|
+
defined?( RUBY_ENGINE ) and RUBY_ENGINE == 'jruby'
|
7
|
+
end
|
8
|
+
|
9
|
+
if ENV[ 'COVERAGE' ]
|
10
|
+
require 'simplecov'
|
11
|
+
SimpleCov.start
|
12
|
+
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'codeclimate-test-reporter'
|
16
|
+
CodeClimate::TestReporter.start
|
17
|
+
ENV[ 'COVERAGE' ] = 'on'
|
18
|
+
rescue LoadError
|
19
|
+
end unless jruby?
|
20
|
+
|
21
|
+
# EOF #
|
@@ -0,0 +1,63 @@
|
|
1
|
+
---
|
2
|
+
forms:
|
3
|
+
test_form:
|
4
|
+
email:
|
5
|
+
title: Email
|
6
|
+
form_title: Your email
|
7
|
+
error_title: email address
|
8
|
+
password:
|
9
|
+
title: Password
|
10
|
+
msg: Password must contain one lowercase and one uppercase letter and one digit
|
11
|
+
test_inflection_form:
|
12
|
+
name:
|
13
|
+
title: Name
|
14
|
+
address:
|
15
|
+
title: Address
|
16
|
+
state:
|
17
|
+
title: State
|
18
|
+
author:
|
19
|
+
title: Author
|
20
|
+
friends:
|
21
|
+
title: Friends
|
22
|
+
chars:
|
23
|
+
title: Characters
|
24
|
+
keywords:
|
25
|
+
title: Keywords
|
26
|
+
notes:
|
27
|
+
title: Notes
|
28
|
+
test_localize_form:
|
29
|
+
simple:
|
30
|
+
title: String
|
31
|
+
utf:
|
32
|
+
title: "ěščřžýáíéúůďťň"
|
33
|
+
yaml:
|
34
|
+
title: "%"
|
35
|
+
msg: "{}"
|
36
|
+
test_localized_steps_form:
|
37
|
+
steps:
|
38
|
+
one: First
|
39
|
+
two: Second
|
40
|
+
three: Third
|
41
|
+
test_r18n_form:
|
42
|
+
msg:
|
43
|
+
title: Message
|
44
|
+
msg2:
|
45
|
+
title: Second Message
|
46
|
+
test_steps_form:
|
47
|
+
steps:
|
48
|
+
intro: Intro
|
49
|
+
email: Email
|
50
|
+
name: Name
|
51
|
+
address: Address
|
52
|
+
message: Message
|
53
|
+
test_time_types_form:
|
54
|
+
time:
|
55
|
+
placeholder: YYYY-MM-DD HH:MM:SS
|
56
|
+
us_date:
|
57
|
+
placeholder: MM/DD/YYYY
|
58
|
+
uk_date:
|
59
|
+
placeholder: DD/MM/YYYY
|
60
|
+
eu_date:
|
61
|
+
placeholder: D.M.YYYY
|
62
|
+
hours:
|
63
|
+
placeholder: HH:MM
|
data/test/r18n/cs.yml
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Form input localization testing messages.
|
2
|
+
|
3
|
+
forms:
|
4
|
+
test_r18n_form:
|
5
|
+
msg:
|
6
|
+
title: Zpráva
|
7
|
+
form_title: Vaše zpráva
|
8
|
+
error_title: Parametr Zpráva
|
9
|
+
msg: '%p není správně vyplněn'
|
10
|
+
match_msg: '%p není ve správném tvaru'
|
11
|
+
reject_msg: '%p obsahuje nepovolené znaky'
|
12
|
+
required_msg: '%p musí být vyplněn'
|
13
|
+
msg2:
|
14
|
+
title: Druhá zpráva
|
15
|
+
error_title: Parametr druhá zpráva
|
16
|
+
msg3:
|
17
|
+
title: Třetí zpráva
|
18
|
+
test_msg: Argument %1
|
19
|
+
inflected_msg: !!inflect
|
20
|
+
s: Singular
|
21
|
+
p: Plural
|
22
|
+
texts:
|
23
|
+
test: Test
|
24
|
+
|
25
|
+
test_inflection_form:
|
26
|
+
name:
|
27
|
+
title: Jméno
|
28
|
+
gender: n
|
29
|
+
address:
|
30
|
+
title: Adresa
|
31
|
+
gender: f
|
32
|
+
state:
|
33
|
+
title: Stát
|
34
|
+
gender: mi
|
35
|
+
author:
|
36
|
+
title: Autor
|
37
|
+
gender: ma
|
38
|
+
keywords:
|
39
|
+
title: Klíčová slova
|
40
|
+
gender: n
|
41
|
+
notes:
|
42
|
+
title: Poznámky
|
43
|
+
gender: f
|
44
|
+
chars:
|
45
|
+
title: Znaky
|
46
|
+
gender: mi
|
47
|
+
friends:
|
48
|
+
title: Přátelé
|
49
|
+
gender: ma
|
50
|
+
test:
|
51
|
+
inflect: invalid
|
52
|
+
|
53
|
+
test_localized_steps_form:
|
54
|
+
steps:
|
55
|
+
one: První
|
56
|
+
two: Druhý
|
57
|
+
three: Třetí
|
58
|
+
four: Should not be here.
|
59
|
+
|
60
|
+
# EOF #
|