kiss 1.1 → 1.7

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