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,33 @@
1
+ module Rack
2
+ autoload :Bench, 'kiss/rack/bench'
3
+ autoload :ErrorsOK, 'kiss/rack/errors_ok'
4
+ autoload :EmailErrors, 'kiss/rack/email_errors'
5
+ autoload :Facebook, 'kiss/rack/facebook'
6
+ autoload :FileNotFound, 'kiss/rack/file_not_found'
7
+ autoload :LogExceptions, 'kiss/rack/log_exceptions'
8
+ autoload :Recorder, 'kiss/rack/recorder'
9
+ autoload :ShowDebug, 'kiss/rack/show_debug'
10
+ autoload :ShowExceptions, 'kiss/rack/show_exceptions'
11
+
12
+ class Request
13
+ def server
14
+ url = scheme + "://"
15
+ url << host
16
+
17
+ if scheme == "https" && port != 443 ||
18
+ scheme == "http" && port != 80
19
+ url << ":#{port}"
20
+ end
21
+
22
+ url
23
+ end
24
+ end
25
+
26
+ class Response
27
+ def prepend_html(*args)
28
+ b = body
29
+ (b = b.join) if b.is_a?(Array)
30
+ b.prepend_html(*args)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ class Kiss
2
+ # This module is included into Sequel database class to provide Kiss-specific
3
+ # fnctionality to database objects.
4
+ module SequelDatabase
5
+
6
+ def self.append_features(mod)
7
+ mod.class_eval do
8
+ alias_method :execute_old, :execute
9
+ _attr_accessor :kiss_controller, :kiss_request, :kiss_model_cache
10
+
11
+ def execute(sql, *args, &block) #:nodoc:
12
+ @_last_query = sql
13
+ execute_old(sql, *args, &block)
14
+ end
15
+ end
16
+ super
17
+ end
18
+
19
+ @_last_query = nil
20
+ def last_query #:nodoc:
21
+ @_last_query
22
+ end
23
+
24
+ # Returns Sequel dataset to evolution_number table, which specifies
25
+ # app's current evolution number.
26
+ # Creates evolution_number table if it does not exist.
27
+ def evolution_number_table
28
+ unless self.table_exists?(:evolution_number)
29
+ self.create_table :evolution_number do
30
+ column :version, :integer, :null=> false
31
+ end
32
+ self[:evolution_number].insert(:version => 0)
33
+ end
34
+ self[:evolution_number]
35
+ end
36
+
37
+ # Returns app's current evolution number.
38
+ def evolution_number
39
+ evolution_number_table.first.version
40
+ end
41
+
42
+ # Sets app's current evolution number.
43
+ def evolution_number=(version)
44
+ evolution_number_table.update(:version => version)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ class Kiss
2
+ module SequelMySQLDataset
3
+ # Returns results from dataset query as array of arrays,
4
+ # instead of array of hashes.
5
+ def all_arrays(opts = nil, &block)
6
+ a = []
7
+ fetch_arrays(select_sql()) {|r| a << r}
8
+ a.each(&block) if block
9
+ a
10
+ end
11
+
12
+ # Fixes bug in Sequel 1.5; shouldn't be needed for Sequel 2.x
13
+ # (need to double-check, however).
14
+ def fetch_arrays(sql)
15
+ execute(sql) do |r|
16
+ while row = r.fetch_row
17
+ yield row
18
+ end
19
+ end
20
+ self
21
+ end
22
+ end
23
+ end
@@ -1,146 +1,228 @@
1
- module Digest; end
1
+ require 'kiss/form/field';
2
2
 
3
- class Kiss
4
- class Form
5
- module AttributesSetter
6
- def set_attributes(attrs_original,required = [])
7
- unless attrs_original.is_a?(Hash) then
8
- raise "first parameter must be a hash of attributes: #{attrs_original}"
9
- end
10
-
11
- attrs = attrs_original.clone
12
-
13
- required.each do |key|
14
- raise "missing required parameter '#{key}'" unless attrs[key]
15
- send("#{key}=", attrs[key])
16
- attrs.delete(key)
17
- end
18
-
19
- attrs.each_pair do |key,value|
20
- send("#{key}=", value)
21
- end
22
- end
23
- end
24
- include Kiss::Form::AttributesSetter
3
+ # Part Hash, part Array... it's a Hashay.
4
+ class Hashay < Hash
5
+ _attr_accessor :keys, :values
25
6
 
26
- require 'kiss/form/field';
7
+ def initialize(*args, &block)
8
+ super
9
+ @_keys = []
10
+ @_values = []
11
+ end
27
12
 
28
- attr_accessor :name,:params,:submitted,:action,:controller,:id,:url,:request,
29
- :object,:fields,:fields_hash,:method,:enctype,:errors,:submit,:cancel,:style,
30
- :has_field_errors,:error_class,:field_error_class,:mark_required,:object, :html
13
+ def each_key(&block)
14
+ @_keys.each &block
15
+ end
31
16
 
17
+ def each_value(&block)
18
+ @_values.each &block
19
+ end
20
+ alias_method :each, :each_value
32
21
 
33
- @@field_types = {
22
+ def []=(key, value)
23
+ unless has_key?(key)
24
+ @_keys << key
25
+ @_values << value
26
+ end
27
+ super
28
+ end
29
+
30
+ def [](key)
31
+ key.is_a?(Numeric) ? @_values[key] : super
32
+ end
33
+
34
+ include Enumerable
35
+ end
36
+
37
+ class Kiss
38
+ class Form
39
+ _attr_accessor :fields, :params, :submitted, :has_field_errors, :has_required_fields,
40
+ :delegate, :controller, :components, :form, :new_object_index
41
+ dsl_accessor :name, :url, :action, :method, :enctype, :errors, :cancel, :mark_required,
42
+ :id, :class, :style, :html, :error_class, :field_error_class, :objects_save_order,
43
+ :object, :prepend_html, :append_html, :year, :timezone
44
+
45
+ @@component_types = {
34
46
  :text => TextField,
35
47
  :hidden => HiddenField,
36
48
  :textarea => TextAreaField,
37
49
  :password => PasswordField,
38
- #:multitext => MultiTextField,
39
50
  :boolean => BooleanField,
40
51
  :file => FileField,
41
52
  :select => SelectField,
42
53
  :radio => RadioField,
43
54
  :checkbox => CheckboxField,
44
55
  :multiselect => MultiSelectField,
45
- :submit => SubmitField,
56
+ :submit => SubmitField
46
57
  }
58
+
59
+ # Create DSL methods for component types
60
+ self.class_eval(
61
+ @@component_types.keys.map do |type|
62
+ "def #{type}(*args, &block); add_component(:#{type}, *args, &block); end; "
63
+ end.join
64
+ )
65
+
66
+ def add_component(type, name, *args, &block)
67
+ attrs = args.to_attrs
68
+ add_field({
69
+ :type => type,
70
+ :name => name
71
+ }.merge(attrs), &block)
72
+ end
47
73
 
74
+ def debug(*args)
75
+ @_delegate.request.debug(args.first, Kernel.caller[0])
76
+ end
48
77
 
49
- # Creates a new form object with specified attributes.
50
- def initialize(attrs)
51
- set_attributes(attrs,[:name,:action])
52
-
53
- raise "Missing required option 'name'" unless @name && (@name != '')
54
-
55
- @method ||= 'post'
78
+ def method_missing(method, *args, &block)
79
+ delegate.send method, *args, &block
80
+ end
56
81
 
57
- # move field definitions to different var
58
- # want to use @fields for array of Field objects
59
- @fields_specs = @fields || []
82
+ def unique(*val)
83
+ if val.empty?
84
+ @_unique
85
+ else
86
+ @_unique << val
87
+ end
88
+ end
60
89
 
61
- @fields = []
62
- @fields_hash = {}
63
- @default_values ||= {}
64
- @params = {}
90
+ # Creates a new form object with specified attributes.
91
+ def initialize(*args, &block)
92
+ @_attrs = args.to_attrs
93
+ _instance_variables_set_from_attrs(@_attrs)
65
94
 
66
- @fields_specs.each do |field|
95
+ @_method ||= 'post'
96
+
97
+ @_components = []
98
+ @_fields = Hashay.new
99
+ @_object_fields = {}
100
+ @_default_values ||= {}
101
+ @_params = {}
102
+ @_with = nil
103
+ @_field_name_prefix = ''
104
+ @_objects_add_order = []
105
+ @_objects_save_order = []
106
+ @_new_object_index = -1
107
+ @_object_fields = {}
108
+ @_unique = []
109
+
110
+ @_prepend_html = ''
111
+ @_append_html = ''
112
+
113
+ (@_attrs[:fields] || []).each do |field|
67
114
  # create field here
68
115
  add_field(field)
69
116
  end
70
117
 
71
- @errors = []
72
- @field_errors = {}
118
+ @_errors = []
119
+ @_field_errors = {}
120
+
121
+ import_instance_variables(@_delegate) if @_delegate
122
+
123
+ instance_eval(&block) if block_given?
124
+
125
+ raise "form name required" unless @_name
126
+ raise "form delegate required" unless @_delegate
73
127
  end
74
128
 
75
- # Adds a section break, which causes form's table to close and new table to open when form is rendered.
76
- def add_section_break
77
- @fields << :section_break
129
+ def context
130
+ @_context ||= begin
131
+ @_timezone ||= (fields['timezone'] && (params['timezone'] || (object && object[:timezone]))) || nil
132
+ {
133
+ :timezone => @_timezone,
134
+ :year => @_year
135
+ }
136
+ end
78
137
  end
79
138
 
80
- def create_field(attrs)
81
- raise "form already has field named '#{attrs[:name]}'" if @fields_hash[attrs[:name]]
139
+ # Creates and adds a field to the form, according to specified attributes.
140
+ def add_field(attrs = {}, &block)
141
+ attrs = @_with.merge(attrs) if @_with
142
+ name = attrs[:name].to_s
143
+ key = (attrs[:key] || name).to_sym
144
+
145
+ name = @_field_name_prefix + name unless @_field_name_prefix.empty?
146
+
147
+ type = attrs[:type] ? attrs[:type].to_sym : :text
148
+ raise "invalid field type '#{type}'" unless @@component_types.has_key?(type)
82
149
 
83
- attrs[:type] ||= :text
84
- type = attrs[:type].to_sym
85
- type = attrs[:type] = :text unless @@field_types.has_key?(type)
150
+ field = @@component_types[type].new(self, attrs.merge(
151
+ :name => name,
152
+ :key => key,
153
+ :type => type
154
+ ), &block)
86
155
 
87
- field_class = @@field_types[type]
88
- field = field_class.new(self,attrs)
156
+ field.object = @_object if @_object && !field.object
157
+ obj = field.object
158
+ # must hash @_object_fields by Ruby object id; if by object, weird lookup errors result,
159
+ # even when lookup object has same Ruby object id as the hash key object!
160
+ ruby_obj_id = obj.object_id
161
+ unless @_object_fields[ruby_obj_id]
162
+ @_object_fields[ruby_obj_id] = {}
163
+ @_objects_add_order << object
164
+ end
165
+ if @_object_fields[ruby_obj_id][field.name]
166
+ raise "duplicate form field name '#{attrs[:name]}'#{" on #{obj.class.name} object" if obj}"
167
+ end
168
+ @_object_fields[ruby_obj_id][field.name] = field
89
169
 
90
- field_name = field.name.to_s
91
- field.form = self
170
+ @_fields[name] = field
171
+ @_components << field
92
172
 
93
173
  while true
94
174
  other_field = field.other_field
95
175
  break unless other_field
96
176
  other_field.form = self
97
- @other_field = @form.create_field( { :name => @name + '.other' }.merge(@other) )
177
+ @_other_field = @_form.create_field( { :name => @_name + '.other' }.merge(@_other) )
98
178
  end
99
179
 
100
- @fields_hash[field_name] = field
101
-
102
- @enctype = 'multipart/form-data' if field.type == :file
180
+ @_enctype = 'multipart/form-data' if field.type == :file
103
181
 
104
182
  field
105
183
  end
184
+ alias_method :create_field, :add_field
106
185
 
107
- # Creates and adds a field to the form, according to specified attributes.
108
- def add_field(attrs)
109
- # create field
110
- field = create_field(attrs)
111
-
112
- # used with mark_required to display required fields legend
113
- @has_required_fields ||= attrs[:required]
114
-
115
- # add field to form array/hash
116
- @fields << field
117
-
118
- field
186
+ # Creates and adds set of submit buttons to the form, per specified attributes.
187
+ def submit(*args, &block)
188
+ if args.size > 0
189
+ raise 'submit already defined' if @_submit
190
+
191
+ attrs = {
192
+ :type => :submit,
193
+ :name => 'submit',
194
+ :save => false,
195
+ :cancel => 'Cancel'
196
+ }
197
+ attrs.merge!(args.pop) if args.last.is_a?(Hash)
198
+ attrs[:options] = args if args.size > 0
199
+ attrs = @_with.merge(attrs) if @_with
200
+ @_submit = @@component_types[:submit].new(self, attrs, &block)
201
+ else
202
+ @_submit
203
+ end
119
204
  end
205
+ alias_method :add_submit, :submit
120
206
 
121
- # Creates and adds set of submit buttons to the form, per specified attributes.
122
- def add_submit(attrs)
123
- @submit = create_field({
124
- :type => :submit,
125
- :name => 'submit',
126
- :save => false
127
- }.merge(attrs) )
128
- end
129
-
130
- # Gets hash of form values ready to be saved to Sequel::Model object.
131
- # def sequel_values
132
- # @sequel_values ||= begin
133
- # hash = {}
134
- # @fields.each {|field| hash[field.name] = field.sequel_value unless field == :section_break }
135
- # hash
136
- # end
137
- # end
207
+ def reset
208
+ @_params = {}
209
+ @_fields.each {|field| field.reset }
210
+ end
211
+
212
+ def field(name)
213
+ @_fields[name]
214
+ end
215
+
216
+ def object_field(obj, name)
217
+ @_object_fields[obj.object_id][name.to_s]
218
+ end
138
219
 
139
220
  # Gets hash of form values.
140
221
  def values
141
- @values ||= begin
222
+ @_values ||= begin
142
223
  hash = {}
143
- @fields.each {|field| hash[field.name] = field.value unless field == :section_break}
224
+ @_fields.each {|field| hash[field.name] = field.value }
225
+ hash['submit'] = @_submit.value = params[@_submit.name.to_s]
144
226
  hash
145
227
  end
146
228
  end
@@ -149,27 +231,26 @@ class Kiss
149
231
  # If multiple args given, first arg is field name, and second arg (error message)
150
232
  # will render next to specified field.
151
233
  def add_error(*args)
152
- if args.size > 2
153
- raise 'too many args'
154
- elsif args.size == 0
155
- raise 'at least one arg required'
156
- end
157
-
158
- # args.size == 1 or 2
159
- if args.size == 1
160
- if args[0].is_a?(String)
161
- @errors << args[0]
162
- return
234
+ case args.size
235
+ when 0
236
+ raise 'at least one argument required'
237
+ when 1
238
+ arg = args.first
239
+ if arg.is_a?(Hash)
240
+ field = arg.field || arg.field_name
241
+ message = arg.message
163
242
  else
164
- field_name = args[0].field_name.to_s
165
- message = args[0].message
243
+ @_errors << arg
244
+ return
166
245
  end
167
- else # args.size == 2
168
- # args == [field_name, message]
169
- field_name = args[0].to_s
170
- message = args[1]
246
+ when 2
247
+ field, message = args
248
+ else
249
+ raise 'too many arguments'
171
250
  end
172
- return @fields_hash[field_name].add_error(message)
251
+
252
+ field = @_fields[field] if field.is_a?(String)
253
+ field.add_error(message)
173
254
  end
174
255
 
175
256
  # Validates form values against fields' format and required attributes,
@@ -177,92 +258,211 @@ class Kiss
177
258
  def validate
178
259
  return nil unless submitted
179
260
 
180
- @fields.each {|field| field.validate unless field == :section_break }
261
+ @_fields.each {|field| field.validate }
262
+ @_unique.each {|field_set| validate_uniqueness_of(field_set) }
181
263
 
182
- has_errors ? nil : values
264
+ has_errors? ? nil : values
183
265
  end
184
266
 
185
- # Saves form values to Sequel::Model object.
186
- def save(object = @object)
187
- @fields.each do |field|
188
- next if field == :section_break
189
- # ignore fields whose name starts with underscore
190
- next if field.name =~ /\A\_/
191
- # don't save 'ignore' fields to the database
192
- next if field.ignore || !field.save
193
-
194
- value = (field.value != nil || object.class.db_schema[field.name.to_sym].allow_null) ?
195
- field.value : (object.class.db_schema[field.name.to_sym][:default] ||= field.format.default)
196
-
197
- object[field.name.to_sym] = field.save == true ? value :
198
- (digest_class = Kiss.digest_class(field.save)) ? digest_class.hexdigest(value) : value
267
+ def validate_uniqueness_of(column_set)
268
+ column_set_labels_values = column_set.map do |column_name|
269
+ (field = fields[column_name.to_s]) ?
270
+ [field.label, field.value] :
271
+ [column_name.to_s.sub(/_id\Z/, '').titlecase, object[column_name]]
199
272
  end
273
+ conditions = column_set.zip column_set_labels_values.map {|lv| lv[1] }
200
274
 
201
- object.save
202
- end
203
-
204
- # Returns true if form has errors.
205
- def has_errors
206
- (@errors.size > 0 || @has_field_errors)
275
+ dataset = @_object.model.filter(conditions)
276
+ unless (@_object.new? ? dataset : dataset.exclude(@_object.pk_hash)).empty?
277
+ labels = column_set_labels_values.map {|lv| lv[0] }
278
+ message = "There is already another #{@_object.model.name.singularize.gsub('_', ' ')} with the same #{labels.conjoin}."
279
+ return (
280
+ (column_set.length == 1) &&
281
+ (field_name = column_set[0].to_str) &&
282
+ fields[field_name]
283
+ ) ? add_error(field_name, message) : add_error(message)
284
+ end
207
285
  end
208
286
 
209
287
  # Checks whether form was submitted and accepted by user and, if so,
210
288
  # whether form validates.
211
289
  # If form validates, saves form values to form's Sequel::Model object
212
290
  # and returns true. Otherwise, returns false.
213
- def process(object = @object)
291
+ def process(*objs)
214
292
  return false unless submitted
215
293
 
216
294
  if accepted
217
295
  validate
218
- return false if has_errors
296
+ return false if has_errors?
219
297
 
220
- save(object)
298
+ save(*objs)
221
299
  end
222
300
 
223
301
  return true
224
302
  end
225
303
 
304
+ def process_or_render
305
+ self.render unless self.process
306
+ end
307
+
308
+ private
309
+
310
+ def extract_objects_from_args(objs)
311
+ objs = args.first if objs.first.is_a?(Array)
312
+ if objs.size == 0
313
+ objects_save_order.push(@_object) if @_object && !objects_save_order.include?(@_object)
314
+ objs = objects_save_order
315
+ end
316
+ objs
317
+ end
318
+
319
+ public
320
+
321
+ def set_object_data(obj = @_object)
322
+ @_object_fields[obj.object_id].values.each do |field|
323
+ # ignore fields whose name starts with underscore
324
+ next if field.name =~ /\A\_/
325
+ # don't save 'ignore' fields to the database
326
+ next if field.ignore || !field.save
327
+ # ignore file fields
328
+ next if field.type == :file
329
+
330
+ key = field.key.to_sym
331
+ value = (field.value != nil || obj.class.db_schema[key].allow_null) ?
332
+ field.value : (obj.class.db_schema[key][:default] ||= field.format.default)
333
+
334
+ if field.digest
335
+ value = Digest.const_get(field.digest.to_sym).hexdigest(value)
336
+ end
337
+
338
+ obj[key] = value if field.save
339
+ end
340
+ end
341
+
342
+ def set_objects_data(*args)
343
+ extract_objects_from_args(args).each do |obj|
344
+ next unless obj
345
+ set_object_data(obj)
346
+ end
347
+ end
348
+
349
+ # Saves form input data associated with specified objects, or
350
+ # all form objects if objects are not specified here.
351
+ def save(*args)
352
+ db.transaction do
353
+ extract_objects_from_args(args).each do |obj|
354
+ set_object_data(obj)
355
+ obj.save
356
+ end
357
+ end
358
+ end
359
+
360
+ def require_values(*names)
361
+ names.each {|name| fields[name.to_s].require_value }
362
+ end
363
+
364
+ def with(*args, &block)
365
+ if block_given?
366
+ attrs = args.to_attrs
367
+ old_with = @_with
368
+ @_with = (@_with || {}).merge(attrs)
369
+
370
+ instance_eval(&block)
371
+
372
+ @_with = old_with
373
+ end
374
+ end
375
+
376
+ def object(obj = nil, options = {}, &block)
377
+ if block_given?
378
+ raise 'missing object arg' unless obj
379
+
380
+ if obj.is_a?(Symbol)
381
+ parent = @_object
382
+ raise 'missing parent object for object symbol reference' unless parent
383
+ obj = parent.send(obj) #||
384
+ #parent.set_associated_object(parent.class.association_reflection(object).associated_class.new)
385
+ end
386
+ raise 'object parameter must reference a model object' unless obj.is_a?(Kiss::Model)
387
+
388
+ # if new, save early to give object a primary key, needed to save assoc children
389
+ @_objects_save_order << obj if obj.new?
390
+ prefix = options[:prefix]
391
+
392
+ old_obj = @_object
393
+ old_prefix = @_field_name_prefix
394
+
395
+ @_object = obj
396
+ @_field_name_prefix = (@_field_name_prefix + prefix.to_s + '.') if prefix
397
+ instance_eval(&block)
398
+ @_object = old_obj
399
+ @_field_name_prefix = old_prefix
400
+
401
+ # save again to pick up any assoc ids from children
402
+ @_objects_save_order << obj
403
+ obj
404
+ elsif obj
405
+ @_object = obj
406
+ else
407
+ @_object
408
+ end
409
+ end
410
+
411
+ # Returns true if form has errors.
412
+ def has_errors?
413
+ (@_errors.size > 0 || @_has_field_errors)
414
+ end
415
+
226
416
  # Returns true if user submitted form with non-cancel submit button
227
417
  # (non-nil submit param, not equal to cancel submit button value).
228
418
  def accepted
229
- raise 'form missing submit field' unless @submit
230
- return params[@submit.name] != @submit.cancel
419
+ raise 'form missing submit field' unless @_submit
420
+ return params[@_submit.name] != @_submit.cancel
231
421
  end
232
422
 
233
423
  # Renders error HTML block for top of form, and returns as string.
234
424
  def errors_html
235
- return nil unless has_errors
425
+ return nil unless has_errors?
236
426
 
237
- @errors << "Please correct the errors highlighted below." if @has_field_errors
427
+ @_errors << "Please correct the #{@_errors.empty? ? '' : 'other '}errors highlighted below." if @_has_field_errors
238
428
 
239
- if @errors.size == 1
240
- content = @errors[0]
429
+ if @_errors.size == 1
430
+ content = @_errors[0]
241
431
  else
242
- content = "<ul>" + @errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
432
+ content = "<ul>" + @_errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
243
433
  end
244
434
 
245
- @errors.pop if @has_field_errors
435
+ @_errors.pop if @_has_field_errors
246
436
 
247
- plural = @errors.size > 1 || @has_field_errors ? 's' : nil
248
- %Q(<div class="kiss_error">#{content}</div><br clear="all" />)
437
+ plural = @_errors.size > 1 || @_has_field_errors ? 's' : nil
438
+ %Q(<table class="kiss_error"><tr><td>#{content}</td></tr></table>)
249
439
  end
250
440
 
251
441
  # Renders current action using form's HTML as action render content.
252
442
  def render(options = {})
253
- @request.render options.merge(:content => html)
443
+ @_delegate.render options.merge(:content => html)
444
+ end
445
+
446
+ def form_name_hidden_tag_html
447
+ %Q(<input type=hidden name="form" value="#{@_name}">)
254
448
  end
255
449
 
256
450
  # Renders beginning of form (form open tag and form/field/error styles).
257
451
  def html_open
258
- @error_class ||= 'kiss_form_error_message'
259
- @field_error_class ||= @error_class
452
+ @_error_class ||= 'kiss_form_error_message'
453
+ @_field_error_class ||= @_error_class
260
454
 
261
455
  # form tag
262
- form_attrs = ['method','enctype','class','style'].map do |attr|
456
+ form_attrs = ['id', 'method', 'enctype', 'class', 'style'].map do |attr|
457
+ next if (value = send attr).blank?
263
458
  "#{attr}=\"#{send attr}\""
264
- end.join(' ')
265
- form_tag = %Q(<form action="#{@action}" #{form_attrs}><input type=hidden name="form" value="#{@name}">)
459
+ end
460
+ if @_html
461
+ @_html.each_pair do |k, v|
462
+ form_attrs.push("#{k}=\"#{v}\"")
463
+ end
464
+ end
465
+ form_tag = %Q(<form action="#{@_action}" #{form_attrs.join(' ')}>#{form_name_hidden_tag_html})
266
466
 
267
467
  # style tag
268
468
  styles = []
@@ -275,15 +475,16 @@ table.kiss_form {
275
475
  vertical-align: middle;
276
476
  }
277
477
  .kiss_form .kiss_error {
478
+ margin-bottom: 4px;
479
+ }
480
+ .kiss_form .kiss_error td {
278
481
  background-color: #ff8;
279
482
  padding: 2px 4px;
280
483
  line-height: 135%;
281
- float: left;
282
484
  color: #900;
283
485
  border: 1px solid #ea4;
284
- margin-bottom: 4px;
285
486
  }
286
- .kiss_form .kiss_error ul {
487
+ .kiss_form .kiss_error td ul {
287
488
  padding-left: 16px;
288
489
  margin: 0;
289
490
  }
@@ -312,11 +513,34 @@ table.kiss_form {
312
513
  .kiss_form tr.kiss_submit td.kiss_submit {
313
514
  padding: 6px 3px;
314
515
  }
315
- .kiss_form input[type="text"],.kiss_form input[type=password],.kiss_form textarea {
516
+ .kiss_form input[type="text"],
517
+ .kiss_form input[type=password],
518
+ .kiss_form textarea {
316
519
  width: 250px;
317
520
  margin-left: 0;
318
521
  margin-right: 0;
319
522
  }
523
+ .kiss_form table.kiss_field_columns {
524
+ border-spacing: 0;
525
+ border-collapse: collapse;
526
+ width: 100%;
527
+ }
528
+ .kiss_form table.kiss_field_columns td {
529
+ padding: 0 16px 2px 0;
530
+ vertical-align: top;
531
+ }
532
+ .kiss_form .kiss_checkbox .kiss_label {
533
+ vertical-align: top;
534
+ padding-top: 3px;
535
+ }
536
+ .kiss_form .kiss_radio .kiss_label {
537
+ vertical-align: top;
538
+ padding-top: 4px;
539
+ }
540
+ .kiss_form .kiss_textarea .kiss_label {
541
+ vertical-align: top;
542
+ padding-top: 3px;
543
+ }
320
544
  EOT
321
545
  )
322
546
  styles.push( <<-EOT
@@ -349,39 +573,35 @@ tr.kiss_form_error_row .kiss_form_error_message {
349
573
  top: -2px;
350
574
  margin-bottom: 0;
351
575
  }
352
- .kiss_form table.kiss_field_columns {
353
- border-spacing: 0;
354
- border-collapse: collapse;
355
- }
356
- .kiss_form table.kiss_field_columns td {
357
- padding: 0 0 2px 0;
358
- }
359
576
  EOT
360
- ) if @error_class == 'kiss_form_error_message'
577
+ ) if @_error_class == 'kiss_form_error_message'
361
578
  style_tag = styles.size == 0 ? '' : "<style>" + styles.join('') + "</style>"
362
579
 
363
580
  # combine
364
- return %Q(#{form_tag}#{style_tag}<div class="kiss_form">#{errors_html})
581
+ return %Q(#{@_prepend_html}#{form_tag}#{style_tag}<div class="kiss_form">#{errors_html})
365
582
  end
366
583
 
367
584
  # Renders end of form (form close tag).
368
585
  def html_close
369
- '</div></form>'
586
+ "</div></form>#{@_append_html}"
370
587
  end
371
588
 
372
589
  # Renders form fields HTML.
373
- def fields_html
374
- @fields.map do |field|
375
- field == :section_break ? begin
376
- table_html_close + table_html_open
377
- end : field_html(field)
590
+ def components_html
591
+ @_components.map do |component|
592
+ component_html(component)
378
593
  end.join
379
594
  end
595
+ alias_method :fields_html, :components_html
380
596
 
381
597
  # Renders open of form table.
382
598
  def table_html_open
383
- %Q(<table class="kiss_form" border=0 cellspacing=0>) +
384
- (@has_required_fields && @mark_required ? %Q( <tr><td></td><td class="kiss_help">Required fields marked by <span class="kiss_required">*</span></td></tr> ) : '')
599
+ %Q(<table class="kiss_form" border=0 cellspacing=0>)
600
+ end
601
+
602
+ def required_legend_html
603
+ (@_has_required_fields && @_mark_required) ?
604
+ %Q( <tr><td></td><td class="kiss_help">Required fields marked by <span class="kiss_required">*</span></td></tr> ) : ''
385
605
  end
386
606
 
387
607
  # Renders close of form table.
@@ -391,7 +611,7 @@ EOT
391
611
 
392
612
  # Renders form submit buttons.
393
613
  def submit_html
394
- @submit ? field_html(@submit) : ''
614
+ @_submit ? field_html(@_submit) : ''
395
615
  end
396
616
 
397
617
  # Renders complete form HTML.
@@ -399,7 +619,8 @@ EOT
399
619
  return [
400
620
  html_open,
401
621
  table_html_open,
402
- fields_html,
622
+ required_legend_html,
623
+ components_html,
403
624
  submit_html,
404
625
  table_html_close,
405
626
  html_close
@@ -407,18 +628,21 @@ EOT
407
628
  end
408
629
 
409
630
  # Renders HTML for specified form field.
410
- def field_html(field)
631
+ def component_html(field)
632
+ field = fields[field.to_s] if (field.is_a?(Symbol) || field.is_a?(String))
633
+ return field.element_html if field.type == :hidden
634
+
411
635
  type = field.type
412
636
  prompt = field.prompt
413
637
  label = field.label
414
638
  errors = field.errors_html
415
- required = field.required ? %Q(<span class="kiss_required">#{@mark_required}</span> ) : ''
639
+ required = field.required ? %Q(<span class="kiss_required">#{@_mark_required}</span> ) : ''
416
640
 
417
641
  ([
418
642
  prompt ? %Q(<tr class="kiss_prompt"><td class="kiss_label">#{required}</td><td>#{prompt.to_s}</td></tr>) : '',
419
643
 
420
644
  %Q(<tr class="kiss_#{type}"><td class="kiss_label#{errors ? ' error' : ''}">),
421
- !prompt ? (required + (label ? label.to_s + ':' : '' )) : '',
645
+ !prompt ? (required + (label.blank? ? '' : label.to_s + ':' )) : '',
422
646
  %Q(</td><td class="kiss_#{type}">),
423
647
  field.element_html, "</td></tr>"
424
648
  ] + (errors ? [
@@ -427,5 +651,6 @@ EOT
427
651
  '</td></tr>'
428
652
  ] : [])).join
429
653
  end
654
+ alias_method :field_html, :component_html
430
655
  end
431
656
  end