kiss 1.1 → 1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/bin/kiss +151 -34
- data/data/scaffold.tgz +0 -0
- data/lib/kiss.rb +389 -742
- data/lib/kiss/accessors/controller.rb +47 -0
- data/lib/kiss/accessors/request.rb +106 -0
- data/lib/kiss/accessors/template.rb +23 -0
- data/lib/kiss/action.rb +502 -132
- data/lib/kiss/bench.rb +14 -5
- data/lib/kiss/debug.rb +14 -6
- data/lib/kiss/exception_report.rb +22 -299
- data/lib/kiss/ext/core.rb +700 -0
- data/lib/kiss/ext/rack.rb +33 -0
- data/lib/kiss/ext/sequel_database.rb +47 -0
- data/lib/kiss/ext/sequel_mysql_dataset.rb +23 -0
- data/lib/kiss/form.rb +404 -179
- data/lib/kiss/form/field.rb +183 -307
- data/lib/kiss/form/field_types.rb +239 -0
- data/lib/kiss/format.rb +88 -70
- data/lib/kiss/html/exception_report.css +222 -0
- data/lib/kiss/html/exception_report.html +210 -0
- data/lib/kiss/iterator.rb +14 -12
- data/lib/kiss/login.rb +8 -8
- data/lib/kiss/mailer.rb +68 -66
- data/lib/kiss/model.rb +323 -36
- data/lib/kiss/rack/bench.rb +16 -8
- data/lib/kiss/rack/email_errors.rb +25 -15
- data/lib/kiss/rack/errors_ok.rb +2 -2
- data/lib/kiss/rack/facebook.rb +6 -6
- data/lib/kiss/rack/file_not_found.rb +10 -8
- data/lib/kiss/rack/log_exceptions.rb +3 -3
- data/lib/kiss/rack/recorder.rb +2 -2
- data/lib/kiss/rack/show_debug.rb +2 -2
- data/lib/kiss/rack/show_exceptions.rb +2 -2
- data/lib/kiss/request.rb +435 -0
- data/lib/kiss/sequel_session.rb +15 -14
- data/lib/kiss/static_file.rb +20 -13
- data/lib/kiss/template.rb +327 -0
- metadata +60 -25
- data/lib/kiss/controller_accessors.rb +0 -81
- data/lib/kiss/hacks.rb +0 -188
- data/lib/kiss/sequel_mysql.rb +0 -25
- data/lib/kiss/template_methods.rb +0 -167
@@ -0,0 +1,239 @@
|
|
1
|
+
# Kiss Form Field types.
|
2
|
+
|
3
|
+
class Kiss
|
4
|
+
class Form
|
5
|
+
class HiddenField < Field; end
|
6
|
+
class TextField < Field; end
|
7
|
+
|
8
|
+
class TextAreaField < Field
|
9
|
+
_attr_accessor :rows, :cols
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@_rows = 5
|
13
|
+
@_cols = 20
|
14
|
+
super(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def element_html(attrs = {})
|
18
|
+
content_tag_html(
|
19
|
+
'textarea',
|
20
|
+
value_string,
|
21
|
+
attrs.merge(
|
22
|
+
:rows => @_rows ||= 1,
|
23
|
+
:cols => @_cols ||= 1
|
24
|
+
)
|
25
|
+
) + tip_html(attrs)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class PasswordField < Field
|
30
|
+
def element_html(*args)
|
31
|
+
input_tag_html(*args)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class FileField < Field
|
36
|
+
def element_html(attrs = {})
|
37
|
+
input_tag_html(attrs) + tip_html(attrs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_file_name; end
|
41
|
+
|
42
|
+
def get_file_data; end
|
43
|
+
|
44
|
+
def require_value(enter_verb)
|
45
|
+
p = param
|
46
|
+
return add_error("Please choose #{label}") unless p && p[:type]
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate
|
50
|
+
require_value(nil) if @_required
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class SubmitField < Field
|
55
|
+
def initialize(*args)
|
56
|
+
@_save = false
|
57
|
+
super(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def element_html(*args)
|
61
|
+
elements_html(*args).join(' ')
|
62
|
+
end
|
63
|
+
|
64
|
+
def elements_html(attrs = {})
|
65
|
+
@_options.map do |option|
|
66
|
+
input_tag_html(attrs.merge( :value => value_to_s(option) ))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# ------ MultiChoiceField
|
72
|
+
|
73
|
+
class MultiChoiceField < Field
|
74
|
+
def initialize(*args, &block)
|
75
|
+
@_options_display_transform = :to_s
|
76
|
+
|
77
|
+
super(*args, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def option_pairs
|
81
|
+
pairs = if @_options_value_key
|
82
|
+
if @_options_display_key.is_a?(Proc)
|
83
|
+
@_options.map {|option| [ option[@_options_value_key], @_options_display_key.call(option) ]}
|
84
|
+
else
|
85
|
+
@_options.map {|option| [
|
86
|
+
option[@_options_value_key] || option.send(@_options_value_key),
|
87
|
+
option[@_options_display_key] || option.send(@_options_display_key)
|
88
|
+
]}
|
89
|
+
end
|
90
|
+
else
|
91
|
+
@_display_format = @_format
|
92
|
+
@_options.map {|option| [ option, option ]}
|
93
|
+
end
|
94
|
+
|
95
|
+
pairs
|
96
|
+
end
|
97
|
+
|
98
|
+
def has_option_value?(v)
|
99
|
+
!(@_options_value_key ?
|
100
|
+
@_options.select {|o| value_to_s(o[@_options_value_key]) == v } :
|
101
|
+
@_options.select {|o| value_to_s(o) == v }
|
102
|
+
).empty?
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate
|
106
|
+
if @_other && param == 'other'
|
107
|
+
@_param = @_form.params[@_name+'.other']
|
108
|
+
end
|
109
|
+
super('select')
|
110
|
+
|
111
|
+
if @_value =~ /\S/ && !has_option_value?(@_value)
|
112
|
+
add_error "Invalid selection"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class SelectField < MultiChoiceField
|
118
|
+
def element_html(attrs = {})
|
119
|
+
return 'No options' unless @_options.size > 0
|
120
|
+
|
121
|
+
@_choose_here ||= 'Choose Here'
|
122
|
+
placeholder_html = %Q(<option value="">#{@_choose_here}</option>)
|
123
|
+
|
124
|
+
options_html = option_pairs.map do |option_value, option_display|
|
125
|
+
option_value_string = value_to_s(option_value)
|
126
|
+
selected = (value_string == option_value_string) ? ' selected' : ''
|
127
|
+
%Q(<option value="#{option_value_string}"#{selected}>#{display_to_s(option_display)}</option>)
|
128
|
+
end.join
|
129
|
+
|
130
|
+
content_tag_html('select', placeholder_html + options_html, attrs) + other_field_html + tip_html(attrs)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class RadioField < MultiChoiceField
|
135
|
+
def element_html(attrs = {})
|
136
|
+
column_layout(elements_html(attrs)) + other_field_html + tip_html(attrs)
|
137
|
+
end
|
138
|
+
|
139
|
+
def elements_html(attrs = {})
|
140
|
+
option_pairs.map do |option_value, option_display|
|
141
|
+
option_value_string = value_to_s(option_value)
|
142
|
+
input_tag_html(
|
143
|
+
attrs.merge( :type => 'radio', :value => option_value_string ),
|
144
|
+
(value_string == option_value_string) ? 'checked' : ''
|
145
|
+
) + @_currency.to_s + display_to_s(option_display)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class BooleanField < RadioField
|
151
|
+
def initialize(*args, &block)
|
152
|
+
@_options = [[1, 'Yes'], [0, 'No']]
|
153
|
+
super(*args, &block)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
# ------ MultiValueField
|
159
|
+
|
160
|
+
class MultiValueField < MultiChoiceField
|
161
|
+
def param
|
162
|
+
@_form.params[@_name.to_s+'[]'] || []
|
163
|
+
end
|
164
|
+
|
165
|
+
def validate
|
166
|
+
begin
|
167
|
+
@_value = param.map { |p| @_format.validate(p) }
|
168
|
+
rescue Kiss::Format::ValidateError => e
|
169
|
+
return add_error("#{e.message.capitalize}")
|
170
|
+
end
|
171
|
+
|
172
|
+
if @_value.empty? && @_required
|
173
|
+
return add_error "Please select at least one #{@_label.downcase.singularize}"
|
174
|
+
end
|
175
|
+
|
176
|
+
if @_min_value_size && @_value.size < @_min_value_size
|
177
|
+
return add_error "Please select at least #{@_min_value_size.of(@_label.downcase)}"
|
178
|
+
end
|
179
|
+
|
180
|
+
if @_max_value_size && @_value.size > @_max_value_size
|
181
|
+
return add_error "Please select no more than #{@_max_value_size.of(@_label.downcase)}"
|
182
|
+
end
|
183
|
+
|
184
|
+
@_value.each do |v|
|
185
|
+
unless has_option_value?(v)
|
186
|
+
return add_error "Invalid selection"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def selected_option_values
|
192
|
+
@_selected_option_values ||= @_value ? Hash[ *(@_value.map {|v| [value_to_s(v), true]}.flatten) ] : {}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class CheckboxField < MultiValueField
|
197
|
+
def element_html(attrs = {})
|
198
|
+
hidden_options = @_hidden_join ? input_tag_html(
|
199
|
+
:type => 'hidden',
|
200
|
+
:name => "#{@_name}_options",
|
201
|
+
:value => option_pairs.map {|option_value, option_display| value_to_s(option_value) }.join(@_hidden_join)
|
202
|
+
) : ''
|
203
|
+
|
204
|
+
column_layout(elements_html(attrs)) + other_field_html + hidden_options + tip_html(attrs)
|
205
|
+
end
|
206
|
+
|
207
|
+
def elements_html(attrs = {})
|
208
|
+
name = @_name.to_s+'[]'
|
209
|
+
option_pairs.map do |option_value, option_display|
|
210
|
+
option_value_string = value_to_s(option_value)
|
211
|
+
|
212
|
+
input_tag_html(
|
213
|
+
attrs.merge( :name => name, :value => option_value_string ),
|
214
|
+
selected_option_values[option_value_string] ? 'checked' : ''
|
215
|
+
) + @_currency.to_s + display_to_s(option_display)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class MultiSelectField < MultiValueField
|
221
|
+
def element_html(attrs = {})
|
222
|
+
options_html = option_pairs.map do |option_value, option_display|
|
223
|
+
option_value_string = value_to_s(option_value)
|
224
|
+
selected = selected_option_values[option_value_string] ? ' selected' : ''
|
225
|
+
%Q(<option value="#{option_value_string}"#{selected}>#{display_to_s(option_display)}</option>)
|
226
|
+
end.join
|
227
|
+
|
228
|
+
content_tag_html(
|
229
|
+
'select',
|
230
|
+
options_html,
|
231
|
+
attrs,
|
232
|
+
'multiple'
|
233
|
+
)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# not implemented yet: MultiTextField,
|
238
|
+
end
|
239
|
+
end
|
data/lib/kiss/format.rb
CHANGED
@@ -4,14 +4,13 @@ class Kiss
|
|
4
4
|
# used to raise exceptions when values don't validate to format regexp
|
5
5
|
class ValidateError < Exception; end
|
6
6
|
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@default = ''
|
7
|
+
@_regexp = nil
|
8
|
+
@_error = 'invalid format'
|
9
|
+
@_legend = nil
|
10
|
+
@_input_width = nil
|
12
11
|
|
13
12
|
class << self
|
14
|
-
|
13
|
+
_attr_accessor :regexp, :error, :legend, :input_width
|
15
14
|
|
16
15
|
# copy Format class instance variables to their subclasses
|
17
16
|
def inherited(subclass)
|
@@ -20,68 +19,68 @@ class Kiss
|
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
23
|
-
def parse(value)
|
22
|
+
def parse(value, context = {})
|
24
23
|
value
|
25
24
|
end
|
26
25
|
|
27
|
-
def value_to_s(value)
|
26
|
+
def value_to_s(value, context = {})
|
28
27
|
value.to_s
|
29
28
|
end
|
30
29
|
|
31
|
-
def validate(value)
|
32
|
-
|
33
|
-
|
30
|
+
def validate(value, context = {})
|
31
|
+
if !value.blank? && @_regexp && value.to_s !~ @_regexp
|
32
|
+
raise Kiss::Format::ValidateError, @_error
|
33
|
+
end
|
34
|
+
parse(value, context)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
38
|
class Integer < self
|
38
|
-
@
|
39
|
-
@
|
40
|
-
@default = 0
|
39
|
+
@_regexp = /\A\-?\d+\Z/
|
40
|
+
@_error = 'must be an integer'
|
41
41
|
|
42
42
|
class << self
|
43
|
-
def parse(value)
|
43
|
+
def parse(value, context = {})
|
44
44
|
value.to_i
|
45
45
|
end
|
46
|
-
|
47
|
-
def validate(value)
|
46
|
+
|
47
|
+
def validate(value, context = {})
|
48
48
|
# remove commas for thousands
|
49
|
-
value
|
49
|
+
value.blank? ? nil : super(value.gsub(/[\$,]/, ''), context)
|
50
50
|
end
|
51
51
|
|
52
|
-
def value_to_s(value)
|
52
|
+
def value_to_s(value, context = {})
|
53
53
|
# add commas for thousands
|
54
|
-
value
|
54
|
+
value
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
59
|
class PositiveInteger < Integer
|
60
|
-
@
|
61
|
-
@
|
60
|
+
@_regexp = /\A\d*[1-9]\d*\Z/
|
61
|
+
@_error = 'must be a positive integer'
|
62
62
|
end
|
63
63
|
|
64
64
|
class UnsignedInteger < Integer
|
65
|
-
@
|
66
|
-
@
|
65
|
+
@_regexp = /\A\d+\Z/
|
66
|
+
@_error = 'must be a positive integer or zero'
|
67
67
|
end
|
68
68
|
|
69
69
|
class NegativeInteger < Integer
|
70
|
-
@
|
71
|
-
@
|
70
|
+
@_regexp = /\A\-\d*[1-9]\d*\Z/
|
71
|
+
@_error = 'must be a negative integer'
|
72
72
|
end
|
73
73
|
|
74
74
|
class Decimal < Integer
|
75
|
-
@
|
76
|
-
@
|
77
|
-
@default = 0
|
75
|
+
@_regexp = /\A\-?(\d+(\.\d*)?|\.\d+)\Z/
|
76
|
+
@_error = 'must be a decimal number'
|
78
77
|
|
79
78
|
class << self
|
80
|
-
def parse(value)
|
79
|
+
def parse(value, context = {})
|
81
80
|
value.to_f
|
82
81
|
end
|
83
82
|
|
84
|
-
def value_to_s(value)
|
83
|
+
def value_to_s(value, context = {})
|
85
84
|
# add commas for thousands, to integer part only
|
86
85
|
value.format_thousands
|
87
86
|
end
|
@@ -89,75 +88,93 @@ class Kiss
|
|
89
88
|
end
|
90
89
|
|
91
90
|
class AlphaNum < self
|
92
|
-
@
|
93
|
-
@
|
91
|
+
@_regexp = /\A[a-z0-9]+\Z/i
|
92
|
+
@_error = 'only letters and numbers allowed'
|
94
93
|
end
|
95
94
|
|
96
95
|
class Word < self
|
97
|
-
@
|
98
|
-
@
|
96
|
+
@_regexp = /\A\w+\Z/
|
97
|
+
@_error = 'only letters, numbers, and underscores allowed'
|
99
98
|
end
|
100
99
|
|
101
100
|
class EmailAddress < self
|
102
|
-
@
|
103
|
-
@
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
101
|
+
@_regexp = /\A[A-Z0-9._%+-]+\@([A-Z0-9\-]+\.)+[A-Z]{2,4}\Z/i
|
102
|
+
@_error = 'must be a valid email address'
|
103
|
+
end
|
104
|
+
|
105
|
+
class URL < self
|
106
|
+
@_regexp = /\Ahttps?:\/\/([A-Z0-9\-]+\.)+[A-Z]{2,4}[^\"\']*\Z/i
|
107
|
+
@_error = 'must be a valid web address, starting with http://'
|
109
108
|
end
|
110
109
|
|
111
110
|
class DateTime < self
|
112
|
-
@
|
113
|
-
@
|
114
|
-
@
|
115
|
-
@
|
116
|
-
@default = SequelZeroTime.new('0000-00-00 00:00:00')
|
111
|
+
@_regexp = /\A\d+\D\d+(\D\d+)?\s+\d{1,2}(\:\d{2})?\s*[ap]m\Z/i
|
112
|
+
@_error = 'must be a valid date and time'
|
113
|
+
@_legend = 'm/d/yyyy h:mm a/pm'
|
114
|
+
@_input_width = 140
|
117
115
|
|
118
116
|
class << self
|
119
|
-
def parse(value)
|
120
|
-
|
117
|
+
def parse(value, context = {})
|
118
|
+
return nil unless value =~ /\S/
|
119
|
+
relative_time = context[:year] ? ::Time.parse("1/1/#{context[:year]}") : ::Time.now
|
120
|
+
convert_value_local_to_utc(::Time.parse(value.gsub(/[-\.]/, '/'), relative_time), context)
|
121
|
+
end
|
122
|
+
|
123
|
+
def convert_value_local_to_utc(value, context)
|
124
|
+
if value && !value.zero? && (timezone = context[:timezone])
|
125
|
+
timezone = TZInfo::Timezone.get(timezone) if timezone.is_a?(String)
|
126
|
+
timezone.local_to_utc(value)
|
127
|
+
else
|
128
|
+
value
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def convert_value_utc_to_local(value, context)
|
133
|
+
if value && !value.zero? && (timezone = context[:timezone])
|
134
|
+
timezone = TZInfo::Timezone.get(timezone) if timezone.is_a?(String)
|
135
|
+
timezone.utc_to_local(value)
|
136
|
+
else
|
137
|
+
value
|
138
|
+
end
|
121
139
|
end
|
122
140
|
|
123
|
-
def value_to_s(value)
|
124
|
-
value.strftime("%m/%d/%Y %I:%M %p").gsub(/0(\d[\:\/])/,'\1')
|
141
|
+
def value_to_s(value, context = {})
|
142
|
+
convert_value_utc_to_local(value, context).strftime("%m/%d/%Y %I:%M %p").gsub(/0(\d[\:\/])/, '\1')
|
125
143
|
end
|
126
144
|
|
127
|
-
def validate(value)
|
128
|
-
value
|
145
|
+
def validate(value, context = {})
|
146
|
+
value.blank? ? nil : super(value, context)
|
129
147
|
end
|
130
148
|
end
|
131
149
|
end
|
132
150
|
|
133
151
|
class Date < DateTime
|
134
|
-
@
|
135
|
-
@
|
136
|
-
@
|
137
|
-
@
|
138
|
-
@default = SequelZeroTime.new('0000-00-00')
|
152
|
+
@_regexp = /\A\d+\D\d+(\D\d+)?\Z/
|
153
|
+
@_error = 'must be a valid date'
|
154
|
+
@_legend = 'm/d/yyyy'
|
155
|
+
@_input_width = 80
|
139
156
|
|
140
|
-
def self.value_to_s(value)
|
157
|
+
def self.value_to_s(value, context = {})
|
141
158
|
value.strftime("%m/%d/%Y")
|
142
159
|
end
|
143
160
|
end
|
144
161
|
|
145
162
|
class Time < DateTime
|
146
|
-
@
|
147
|
-
@
|
148
|
-
@
|
149
|
-
@
|
163
|
+
@_regexp = /\A\d+\:\d+\s*[ap]m\Z/i,
|
164
|
+
@_error = 'must be a valid time'
|
165
|
+
@_legend = 'h:mm a/pm'
|
166
|
+
@_input_width = 80
|
150
167
|
end
|
151
168
|
|
152
169
|
class MonthYear < Date
|
153
|
-
@
|
154
|
-
@
|
155
|
-
@
|
156
|
-
@
|
170
|
+
@_regexp = /\A\d+\D\d+\Z/
|
171
|
+
@_error = 'must be a valid month and year (m/yyyy)'
|
172
|
+
@_legend = 'm/yyyy'
|
173
|
+
@_input_width = 80
|
157
174
|
|
158
175
|
class << self
|
159
|
-
def parse(value)
|
160
|
-
month, year = value.sub(/\A\s*/,'').sub(/\s*\Z/,'').split(/\D+/)
|
176
|
+
def parse(value, context = {})
|
177
|
+
month, year = value.sub(/\A\s*/, '').sub(/\s*\Z/, '').split(/\D+/)
|
161
178
|
# convert two-digit years to four-digit years
|
162
179
|
year = year.to_i
|
163
180
|
if year < 100
|
@@ -171,7 +188,7 @@ class Kiss
|
|
171
188
|
end
|
172
189
|
end
|
173
190
|
|
174
|
-
def value_to_s(value)
|
191
|
+
def value_to_s(value, context = {})
|
175
192
|
value.strftime("%m/%Y")
|
176
193
|
end
|
177
194
|
end
|
@@ -200,6 +217,7 @@ class Kiss
|
|
200
217
|
|
201
218
|
:email => EmailAddress,
|
202
219
|
:email_address => EmailAddress,
|
220
|
+
:url => URL,
|
203
221
|
|
204
222
|
:datetime => DateTime,
|
205
223
|
:date => Date,
|