kiss 1.1 → 1.7
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.
- 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,
|