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
@@ -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'