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,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