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
@@ -1,186 +1,259 @@
1
+ require 'kiss/form/field';
2
+
1
3
  class Kiss
2
4
  class Form
3
5
  class Field
6
+ dsl_accessor :name, :type, :form, :currency, :label, :no_label, :prompt, :value, :read_only,
7
+ :ignore, :save, :options, :options_value_key, :options_display_key, :options_display_transform,
8
+ :required, :unique, :cancel, :columns, :style, :hidden_join, :html, :other, :other_field, :object,
9
+ :format, :display_format, :key, :match, :tip, :statement, :attach_errors, :factor, :digest,
10
+ :min_value_size, :max_value_size, :choose_here
11
+ alias_method :option_value_key, :options_value_key
12
+ alias_method :option_display_key, :options_display_key
13
+ alias_method :option_display_transform, :options_display_transform
14
+ alias_method :min_value_length, :min_value_size
15
+ alias_method :max_value_length, :max_value_size
4
16
 
5
- attr_accessor :name,:type,:form,:currency,:label,:no_label,:prompt,:value,:ignore,:save,
6
- :options,:options_value_key,:options_display_key,:required,:cancel,:columns,:style,:hidden_join,
7
- :html,:other,:other_field,:object,:format,:display_format
17
+ def method_missing(method, *args, &block)
18
+ @_form.action.send method, *args, &block
19
+ end
8
20
 
9
- include Kiss::Form::AttributesSetter
21
+ def options_keys(value, display)
22
+ @_options_value_key = value
23
+ @_options_display_key = display
24
+ end
25
+ alias_method :option_keys, :options_keys
26
+
27
+ def debug(*args)
28
+ @_form.delegate.request.debug(args.first, Kernel.caller[0])
29
+ end
10
30
 
11
- def initialize(form,attrs)
12
- @form = form
13
- @save = true
14
- @currency = nil
31
+ def initialize(form, *args, &block)
32
+ # defaults
33
+ @_form = form
34
+ @_save = true
35
+ @_currency = nil
36
+ @_attrs = args.to_attrs
37
+ @_type = :text
38
+ @_object = @_form.object
39
+ @_save = true
40
+ @_attach_errors = true
41
+
42
+ _instance_variables_set_from_attrs(@_attrs)
43
+ instance_eval(&block) if block_given?
44
+
45
+ @_errors = []
46
+
47
+ @_format = Kiss::Format.lookup(@_format)
48
+ @_display_format = Kiss::Format.lookup(@_display_format)
15
49
 
16
- set_attributes({
17
- :type => :text,
18
- :object => @form.object,
19
- :save => true
20
- }.merge(attrs))
50
+ raise 'field must have a name' unless @_name
51
+ @_key ||= @_name
52
+ @_label ||= @_name.titleize unless @_type == :submit
21
53
 
22
- @value = (@object[@name.to_sym] if @object) || @value
54
+ # object's value overrides any form field value
55
+ # form field value is intended as default in case object value is missing
56
+ if @_object && (value = @_object[@_key.to_sym])
57
+ @_value = value
58
+ end
59
+
60
+ if @_currency.is_a?(Symbol)
61
+ @_currency = case @_currency
62
+ when :dollars
63
+ '$'
64
+ else
65
+ ''
66
+ end
67
+ end
23
68
 
24
- @format = Kiss::Format.lookup(@format)
25
- @display_format = Kiss::Format.lookup(@display_format)
69
+ if @_options
70
+ if @_options[0].is_a?(Array)
71
+ @_options_value_key ||= 0
72
+ (@_options_display_key ||= (@_options[0].size == 1) ? 0 : 1)
73
+ elsif defined?(Kiss::Model) && @_options[0].is_a?(Kiss::Model)
74
+ model_klass = @_options[0].class
75
+ @_options_value_key ||= model_klass.value_column
76
+ @_options_display_key ||= model_klass.display_column
77
+ elsif @_options[0].is_a?(Hash)
78
+ @_options_value_key ||= :id
79
+ @_options_display_key ||= :name
80
+ end
81
+ end
26
82
 
27
- @currency = '$' if @currency == :dollars
83
+ @_tip = ((legend = @_format.legend) ? "(#{legend})" : nil) unless defined? @_tip
28
84
 
29
- @errors = []
85
+ @_form.has_required_fields ||= @_required
30
86
  end
31
87
 
32
88
  def other_field_html
33
- return '' unless @other
89
+ return '' unless @_other
34
90
 
35
- other_checked = @value && !option_pairs.any? {|v,d| v == @value }
91
+ other_checked = @_value && !option_pairs.any? {|v, d| v == @_value }
36
92
 
37
- (@columns ? '<br/>' : '&nbsp; ') + [
93
+ (@_columns ? '<br/>' : '&nbsp; ') + [
38
94
  input_tag_html(
39
- { :value => 'other', :html => { :id => @name+'.other' } },
95
+ { :value => 'other', :html => { :id => @_name+'.other' } },
40
96
  other_checked ? 'checked' : ''
41
97
  ),
42
- @other[:label] || 'Other',
98
+ @_other[:label] || 'Other',
43
99
  ': ',
44
- @currency.to_s,
100
+ @_currency.to_s,
45
101
  input_tag_html({
46
102
  :type => :text,
47
- :name => @name+'.other',
48
- :value => other_checked ? value_to_s(@value) : nil,
103
+ :name => @_name+'.other',
104
+ :value => other_checked ? value_to_s(@_value) : nil,
49
105
  :html => {
50
- :onfocus => "document.getElementById('#{@name}.other').checked = true"
106
+ :onfocus => "document.getElementById('#{@_name}.other').checked = true"
51
107
  }
52
- }.merge(@other))
108
+ }.merge(@_other))
53
109
  ].join
54
110
  end
55
111
 
56
112
  def column_layout(elements_html)
57
- if @columns
58
- # format elements into cells
59
- elements_html.map! { |el| "<td>" + el + "</td>" }
113
+ if elements_html.empty?
114
+ ''
115
+ elsif @_columns
116
+ layout_columns = [@_columns, elements_html.size].min
117
+ num_elements_per_column = ((elements_html.size + layout_columns - 1) / layout_columns).to_i
118
+ layout_columns = ((elements_html.size + num_elements_per_column - 1) / num_elements_per_column).to_i
119
+
120
+ style = "style=\"width: #{(100 / layout_columns).to_i - 1}%\""
60
121
 
61
- # compute height = ceiling(number of elements / columns)
62
- height = ((elements_html.size + columns - 1)/columns).to_i
63
-
64
- # group cells into columns
65
- element_ranges = (1..height).to_a.map {|i| ((i-1)*@columns..(i*@columns-1))}
66
- return ['<table class="kiss_field_columns">',element_ranges.map do |range|
67
- ["<tr>", elements_html[range], "</tr>"]
68
- end,'</table>'].flatten.join
122
+ '<table class="kiss_field_columns"><tr>' +
123
+ (0...layout_columns).map do |i|
124
+ "<td #{style}>" + elements_html[i * num_elements_per_column, num_elements_per_column].join('<br/>') + "</td>"
125
+ end.join + '</table>'
126
+ else
127
+ elements_html.map {|h| "<nobr>#{h}</nobr>"}.join('&nbsp; ')
69
128
  end
70
-
71
- # else columns <= 1
72
- elements_html.join('&nbsp; ')
73
129
  end
74
130
 
75
- # def sequel_value
76
- # case @format
77
- # when :date:
78
- # return Kiss.mdy_to_ymd(value)
79
- # when :datetime:
80
- # datetime = value.sub(/([ap]m)\s*\Z/i,' \1')
81
- # date, time, ampm = datetime.split(/\s+/)
82
- #
83
- # hours, minutes = time.split(/:/)
84
- # hours = 0 if hours.to_i == 12 && ampm
85
- # if (ampm.downcase == 'pm')
86
- # hours = hours.to_i + 12
87
- # end
88
- #
89
- # return Kiss.mdy_to_ymd(date) + " #{hours}:#{minutes}"
90
- # when :month_year:
91
- # month, year = value.sub(/\A\s*/,'').sub(/\s*\Z/,'').split(/\D+/)
92
- # # convert two-digit years to four-digit years
93
- # year = year.to_i
94
- # if year < 100
95
- # year += 1900
96
- # year += 100 if year < Time.now.year - 95
97
- # end
98
- # return sprintf("%04d-%02d-01",year,month.to_i)
99
- # else
100
- # return value.is_a?(Array) ? value.map {|v| v.gsub(/,/,'\,')}.join(',') : value
101
- # end
102
- # end
103
-
104
131
  def param
105
- @param ||= @form.params[@name.to_s]
132
+ @_param ||= @_form.params[@_name.to_s]
106
133
  end
107
134
 
108
135
  def value
109
- @value
136
+ @_value
137
+ end
138
+
139
+ def value_string
140
+ v = value
141
+ v *= @_factor if v && @_factor
142
+ param || value_to_s(v)
110
143
  end
111
144
 
112
145
  def add_error(message)
113
- @errors << message
114
- @form.has_field_errors = true
146
+ (@_attach_errors ? @_errors : form.errors) << message
147
+ @_form.has_field_errors = true
148
+ nil
115
149
  end
116
150
 
117
151
  def value_to_s(value)
118
- value ? @format.value_to_s(value) : ''
152
+ value ? @_format.value_to_s(value, @_form.context) : ''
119
153
  end
120
154
 
121
155
  def display_to_s(value)
122
- value ? @display_format.value_to_s(value) : ''
156
+ value ? @_display_format.value_to_s(value).send(@_options_display_transform) : ''
123
157
  end
124
158
 
125
- def validate(enter = 'enter')
126
- if @required && (param !~ /\S/)
159
+ def require_value(enter_verb = 'enter')
160
+ if (param !~ /\S/)
127
161
  # value required
128
- add_error("Please #{enter} #{@label}")
162
+ add_error("Please #{enter_verb} #{@_label}")
129
163
  return
130
164
  end
165
+ end
166
+
167
+ def reset
168
+ @_value = @_default_value if defined?(@_default_value)
169
+ @_param = nil
170
+ end
171
+
172
+ def validate(enter_verb = 'enter')
173
+ @_default_value ||= value
174
+
175
+ require_value(enter_verb) if @_required
131
176
 
132
177
  begin
133
- @value = @format.validate(param)
178
+ p = @_format.validate(param, @_form.context)
179
+ p *= @_factor if @_factor
180
+ value = p
134
181
  rescue Kiss::Format::ValidateError => e
135
- add_error("#{e.message.capitalize}")
182
+ return add_error("#{e.message.capitalize}")
183
+ end
184
+
185
+ if @_match
186
+ match_field = @_form.fields[@_match.to_s]
187
+ if value != match_field.value
188
+ return add_error("#{match_field.label.pluralize} don't match.")
189
+ end
190
+ end
191
+
192
+ if @_save && @_unique
193
+ dataset = @_object.model.filter(@_name.to_sym => value)
194
+ unless (@_object.new? ? dataset : dataset.exclude(@_object.pk_hash)).empty?
195
+ return add_error("There is already another #{@_object.model.name.singularize.gsub('_', ' ')} with the same #{@_label.downcase.gsub('_', ' ')}.")
196
+ end
136
197
  end
137
198
 
138
- @value
199
+ @_value = value
139
200
  end
140
201
 
141
202
  def errors_html
142
- return nil unless @errors.size > 0
203
+ return nil unless @_errors.size > 0
143
204
 
144
- if @errors.size == 1
145
- content = @errors[0]
205
+ if @_errors.size == 1
206
+ content = @_errors[0]
146
207
  else
147
- content = "<ul>" + @errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
208
+ content = "<ul>" + @_errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
148
209
  end
149
210
 
150
- %Q(<span class="#{@form.field_error_class}">#{content}</span><br clear="all" />)
211
+ %Q(<span class="#{@_form.field_error_class}">#{content}</span><br clear="all" />)
151
212
  end
152
213
 
153
- def element_html
154
- attrs = { :value => param || value_to_s(@value) }
155
- if width = @format.input_width
156
- attrs[:style] = "width: #{width}px"
214
+ def element_html(attrs = {})
215
+ attrs.merge!(
216
+ :value => value_string
217
+ )
218
+
219
+ if width = @_format.input_width
220
+ attrs[:style] ||= "width: #{width}px"
157
221
  end
158
222
 
159
- @currency.to_s + input_tag_html( attrs ) +
160
- ((legend = @format.legend) ? " <small>(#{legend})</small>" : '')
223
+ @_currency.to_s + input_tag_html( attrs ) + tip_html(attrs)
224
+ end
225
+
226
+ def table_row_html
227
+ form.component_html(self)
228
+ end
229
+
230
+ def tip_html(attrs)
231
+ (tip = attrs.has_key?(:tip) ? attrs[:tip] : @_tip) ? " <small>#{tip}</small>" : ''
161
232
  end
162
233
 
163
- def html
234
+ def html(*args)
164
235
  errors = errors_html
165
- element_html + (errors ? (@columns ? '' : '<br/>') + %Q(#{errors}) : '')
236
+ element_html(*args) + (errors ? (@_columns ? '' : '<br/>') + %Q(#{errors}) : '')
166
237
  end
167
238
 
168
239
  def tag_start_html(tag_name, attrs = {}, extra_html = nil)
169
240
  attrs = attrs.clone
170
- attrs[:name] ||= @name
171
- attrs[:size] ||= @size if @size
172
- attrs[:style] ||= @style if @style
241
+ attrs[:name] ||= @_name
242
+ attrs[:size] ||= @_size if @_size
243
+ attrs[:style] ||= @_style if @_style
244
+
245
+ read_only = attrs.delete(:read_only) || @_read_only
173
246
 
174
247
  html_parts = ["<#{tag_name}"]
175
248
 
176
- html = attrs.delete(:html) || @html
177
- attrs.each_pair {|k,v| html_parts.push %Q(#{k}="#{v}") }
249
+ html = attrs.delete(:html) || @_html
250
+ attrs.each_pair {|k, v| html_parts.push %Q(#{k}="#{v}") }
178
251
 
179
252
  if html
180
253
  if html.is_a?(Hash)
181
- html.each_pair {|k,v| html_parts.push %Q(#{k}="#{v}") }
254
+ html.each_pair {|k, v| html_parts.push %Q(#{k}="#{v}") }
182
255
  else
183
- html_parts.push(@html.to_s)
256
+ html_parts.push(@_html.to_s)
184
257
  end
185
258
  end
186
259
 
@@ -189,222 +262,25 @@ class Kiss
189
262
  end
190
263
 
191
264
  def tag_html(tag_name, attrs = {}, extra_html = nil)
192
- tag_start_html(tag_name,attrs,extra_html) + ' />'
265
+ return attrs[:value].html_escape.to_s.gsub(/\n/, '<br/>') if attrs[:read_only] || @_read_only
266
+
267
+ tag_start_html(tag_name, attrs, extra_html) + ' />'
193
268
  end
194
269
 
195
270
  def content_tag_html(tag_name, content, attrs = {}, extra_html = nil)
196
- tag_start_html(tag_name,attrs,extra_html) + ">#{content}</#{tag_name}>"
271
+ return content.to_s.html_escape.gsub(/\n/, '<br/>') if attrs[:read_only] || @_read_only
272
+ tag_start_html(tag_name, attrs, extra_html) + ">#{content}</#{tag_name}>"
197
273
  end
198
274
 
199
275
  def input_tag_html(attrs = {}, extra_html = nil)
200
276
  tag_html(
201
277
  'input',
202
- {:type => @type}.merge(attrs),
278
+ {:type => @_type}.merge(attrs),
203
279
  extra_html
204
280
  )
205
281
  end
206
282
  end
207
-
208
- class HiddenField < Field; end
209
- class TextField < Field; end
210
-
211
- class TextAreaField < Field
212
- attr_accessor :rows,:cols
213
-
214
- def initialize(*args)
215
- @rows = 5
216
- @cols = 20
217
- super(*args)
218
- end
219
-
220
- def element_html
221
- content_tag_html(
222
- 'textarea',
223
- param || value_to_s(@value),
224
- {
225
- :rows => @rows ||= 1,
226
- :cols => @cols ||= 1
227
- }
228
- )
229
- end
230
- end
231
-
232
- class PasswordField < Field
233
- def element_html
234
- input_tag_html
235
- end
236
- end
237
-
238
- class BooleanField < Field
239
- def element_html
240
- input_tag_html({ :value => 1 }, @value ? 'checked' : '')
241
- end
242
- end
243
-
244
- class FileField < Field
245
- def element_html
246
- input_tag_html
247
- end
248
-
249
- def get_file_name
250
-
251
- end
252
-
253
- def get_file_data
254
-
255
- end
256
- end
257
-
258
- class SubmitField < Field
259
- def initialize(*args)
260
- @save = false
261
- super(*args)
262
- end
263
-
264
- def element_html
265
- elements_html.join(' ')
266
- end
267
-
268
- def elements_html
269
- @options.map do |option|
270
- input_tag_html({ :value => value_to_s(option) })
271
- end
272
- end
273
- end
274
-
275
- # ------ MultiChoiceField
276
-
277
- class MultiChoiceField < Field
278
- def option_pairs
279
- (defined? @options_value_key) ?
280
- @options.map {|option| [ option[@options_value_key], option[@options_display_key] ]} :
281
- begin
282
- @display_format = @format
283
- @options.map {|option| [ option, option ] }
284
- end
285
- end
286
-
287
- def matched_options(value)
288
- value = [value].flatten
289
- @options_value_key ?
290
- @options.select {|o| value.select {|v| o[@options_value_key] == v } }.flatten :
291
- @options.select {|o| value.select {|v| o == v } }.flatten
292
- end
293
-
294
- def validate
295
- if @other && param == 'other'
296
- @param = @form.params[@name+'.other']
297
- end
298
- super('select')
299
-
300
- if @value =~ /\S/ && matched_options(@value).size == 0
301
- add_error "Invalid selection"
302
- end
303
- end
304
- end
305
-
306
- class SelectField < MultiChoiceField
307
- def element_html
308
- return 'No options' unless @options.size > 0
309
-
310
- placeholder_html = %Q(<option value="">Choose Here</option>)
311
-
312
- options_html = option_pairs.map do |value,display|
313
- selected = (@value.to_s == value.to_s) ? ' selected' : ''
314
- %Q(<option value="#{value_to_s(value)}"#{selected}>#{display}</option>)
315
- end.join
316
-
317
- content_tag_html('select', placeholder_html + options_html) + other_field_html
318
- end
319
- end
320
-
321
- class RadioField < MultiChoiceField
322
- def element_html
323
- column_layout(elements_html) + other_field_html
324
- end
325
-
326
- def elements_html
327
- option_pairs.map do |value,display|
328
- input_tag_html(
329
- { :value => value_to_s(value) },
330
- (@value.to_s == value.to_s) ? 'checked' : ''
331
- ) + @currency.to_s + display_to_s(display)
332
- end
333
- end
334
- end
335
-
336
-
337
- # ------ MultiValueField
338
-
339
- class MultiValueField < MultiChoiceField
340
- def param
341
- @form.params[@name.to_s+'[]'] || []
342
- end
343
-
344
- def validate
345
- begin
346
- @value = param.map { |p| @format.validate(p) }
347
- rescue Kiss::Format::ValidateError => e
348
- add_error("#{e.message.capitalize}")
349
- return
350
- end
351
-
352
- if @value.size > 0
353
- @value.each do |v|
354
- unless (matched_options(v).size > 0)
355
- add_error "#Invalid selection"
356
- return
357
- end
358
- end
359
- elsif @required
360
- add_error "Please select at least one #{@label}"
361
- end
362
- end
363
-
364
- def selected_option_values
365
- @selected_option_values ||= @value ? Hash[ *(@value.map {|v| [v.to_s,true]}.flatten) ] : {}
366
- end
367
- end
368
-
369
- class CheckboxField < MultiValueField
370
- def element_html
371
- hidden_options = @hidden_join ? input_tag_html(
372
- :type => 'hidden',
373
- :name => "#{@name}_options",
374
- :value => option_pairs.map {|v,d| value_to_s(v) }.join(@hidden_join)
375
- ) : ''
376
-
377
- column_layout(elements_html) + other_field_html + hidden_options
378
- end
379
-
380
- def elements_html
381
- name = @name.to_s+'[]'
382
- option_pairs.map do |value,display|
383
- input_tag_html(
384
- { :name => name, :value => value_to_s(value) },
385
- selected_option_values[value.to_s] ? 'checked' : ''
386
- ) + @currency.to_s + display_to_s(display)
387
- end
388
- end
389
- end
390
-
391
- class MultiSelectField < MultiValueField
392
- def element_html
393
- options_html = option_pairs.map do |value,display|
394
- selected = selected_option_values[value] ? ' selected' : ''
395
- %Q(<option value="#{value_to_s(value)}"#{selected}>#{display}</option>)
396
- end.join
397
-
398
- content_tag_html(
399
- 'select',
400
- options_html,
401
- {},
402
- 'multiple'
403
- )
404
- end
405
- end
406
283
  end
407
284
  end
408
285
 
409
- # not implemented yet:
410
- # :multitext => MultiTextField,
286
+ require 'kiss/form/field_types'