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.
Files changed (45) hide show
  1. data/LICENSE +1 -1
  2. data/Rakefile +2 -1
  3. data/VERSION +1 -1
  4. data/bin/kiss +151 -34
  5. data/data/scaffold.tgz +0 -0
  6. data/lib/kiss.rb +389 -742
  7. data/lib/kiss/accessors/controller.rb +47 -0
  8. data/lib/kiss/accessors/request.rb +106 -0
  9. data/lib/kiss/accessors/template.rb +23 -0
  10. data/lib/kiss/action.rb +502 -132
  11. data/lib/kiss/bench.rb +14 -5
  12. data/lib/kiss/debug.rb +14 -6
  13. data/lib/kiss/exception_report.rb +22 -299
  14. data/lib/kiss/ext/core.rb +700 -0
  15. data/lib/kiss/ext/rack.rb +33 -0
  16. data/lib/kiss/ext/sequel_database.rb +47 -0
  17. data/lib/kiss/ext/sequel_mysql_dataset.rb +23 -0
  18. data/lib/kiss/form.rb +404 -179
  19. data/lib/kiss/form/field.rb +183 -307
  20. data/lib/kiss/form/field_types.rb +239 -0
  21. data/lib/kiss/format.rb +88 -70
  22. data/lib/kiss/html/exception_report.css +222 -0
  23. data/lib/kiss/html/exception_report.html +210 -0
  24. data/lib/kiss/iterator.rb +14 -12
  25. data/lib/kiss/login.rb +8 -8
  26. data/lib/kiss/mailer.rb +68 -66
  27. data/lib/kiss/model.rb +323 -36
  28. data/lib/kiss/rack/bench.rb +16 -8
  29. data/lib/kiss/rack/email_errors.rb +25 -15
  30. data/lib/kiss/rack/errors_ok.rb +2 -2
  31. data/lib/kiss/rack/facebook.rb +6 -6
  32. data/lib/kiss/rack/file_not_found.rb +10 -8
  33. data/lib/kiss/rack/log_exceptions.rb +3 -3
  34. data/lib/kiss/rack/recorder.rb +2 -2
  35. data/lib/kiss/rack/show_debug.rb +2 -2
  36. data/lib/kiss/rack/show_exceptions.rb +2 -2
  37. data/lib/kiss/request.rb +435 -0
  38. data/lib/kiss/sequel_session.rb +15 -14
  39. data/lib/kiss/static_file.rb +20 -13
  40. data/lib/kiss/template.rb +327 -0
  41. metadata +60 -25
  42. data/lib/kiss/controller_accessors.rb +0 -81
  43. data/lib/kiss/hacks.rb +0 -188
  44. data/lib/kiss/sequel_mysql.rb +0 -25
  45. 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
@@ -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
- @regexp = //
8
- @error = 'invalid format'
9
- @legend = nil
10
- @input_width = nil
11
- @default = ''
7
+ @_regexp = nil
8
+ @_error = 'invalid format'
9
+ @_legend = nil
10
+ @_input_width = nil
12
11
 
13
12
  class << self
14
- attr_accessor :regexp, :error, :legend, :input_width, :default
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
- raise Kiss::Format::ValidateError, @error unless value.to_s =~ @regexp
33
- parse(value)
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
- @regexp = /\A\-?\d+\Z/
39
- @error = 'must be an integer'
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 =~ /\d/ ? super(value.gsub(/,/,'')) : nil
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.format_thousands
54
+ value
55
55
  end
56
56
  end
57
57
  end
58
58
 
59
59
  class PositiveInteger < Integer
60
- @regexp = /\A\d*[1-9]\d*\Z/
61
- @error = 'must be a positive integer'
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
- @regexp = /\A\d+\Z/
66
- @error = 'must be a positive integer or zero'
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
- @regexp = /\A\-\d*[1-9]\d*\Z/
71
- @error = 'must be a negative integer'
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
- @regexp = /\A\-?(\d+(\.\d*)?|\.\d+)\Z/
76
- @error = 'must be a decimal number'
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
- @regexp = /\A[a-z0-9]\Z/i
93
- @error = 'only letters and numbers allowed'
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
- @regexp = /\A\w+\Z/
98
- @error = 'only letters, numbers, and underscores allowed'
96
+ @_regexp = /\A\w+\Z/
97
+ @_error = 'only letters, numbers, and underscores allowed'
99
98
  end
100
99
 
101
100
  class EmailAddress < self
102
- @regexp = /\A[A-Z0-9._%+-]+\@([A-Z0-9-]+\.)+[A-Z]{2,4}\Z/i
103
- @error = 'must be a valid email address'
104
-
105
- def validate(value)
106
- debug 'hi'
107
- super(value)
108
- end
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
- @regexp = /\A\d+\D\d+(\D\d+)?\s+\d{1,2}\:\d{2}\s*[ap]m\Z/i
113
- @error = 'must be a valid date and time'
114
- @legend = 'm/d/yyyy h:mm a/pm'
115
- @input_width = 140
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
- ::Time.parse(value.gsub(/[-\.]/,'/'))
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 =~ /\S/ ? super(value) : nil
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
- @regexp = /\A\d+\D\d+(\D\d+)?\Z/
135
- @error = 'must be a valid date'
136
- @legend = 'm/d/yyyy'
137
- @input_width = 80
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
- @regexp = /\A\d+\:\d+\s*[ap]m\Z/i,
147
- @error = 'must be a valid time'
148
- @legend = 'h:mm a/pm'
149
- @input_width = 80
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
- @regexp = /\A\d+\D\d+\Z/
154
- @error = 'must be a valid month and year (m/yyyy)'
155
- @legend = 'm/yyyy'
156
- @input_width = 80
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,