kiss 0.9

Sign up to get free protection for your applications and to get access to all the features.
data/lib/kiss/form.rb ADDED
@@ -0,0 +1,414 @@
1
+ class Kiss
2
+ class Form
3
+ module AttributesSetter
4
+ def set_attributes(attrs_original,required = [])
5
+ unless attrs_original.is_a?(Hash) then
6
+ raise "first parameter must be a hash of attributes: #{attrs_original}"
7
+ end
8
+
9
+ attrs = attrs_original.clone
10
+
11
+ required.each do |key|
12
+ raise "missing required parameter '#{key}'" unless attrs[key]
13
+ send("#{key}=", attrs[key])
14
+ attrs.delete(key)
15
+ end
16
+
17
+ attrs.each_pair do |key,value|
18
+ send("#{key}=", value)
19
+ end
20
+ end
21
+ end
22
+ include Kiss::Form::AttributesSetter
23
+
24
+ require 'kiss/form/field';
25
+
26
+ attr_accessor(
27
+ :name,:params,:submitted,:action,:controller,:id,:url,:request,
28
+ :object,:fields,:fields_hash,:method,:enctype,:errors,:submit,:cancel,:style,:class,
29
+ :has_field_errors,:error_class,:field_error_class,:mark_required,:object
30
+ )
31
+
32
+
33
+ @@field_types = {
34
+ :text => TextField,
35
+ :hidden => HiddenField,
36
+ :textarea => TextAreaField,
37
+ :password => PasswordField,
38
+ #:multitext => MultiTextField,
39
+ :boolean => BooleanField,
40
+ :file => FileField,
41
+ :select => SelectField,
42
+ :radio => RadioField,
43
+ :checkbox => CheckboxField,
44
+ :multiselect => MultiSelectField,
45
+ :submit => SubmitField,
46
+ }
47
+
48
+
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'
56
+
57
+ # move field definitions to different var
58
+ # want to use @fields for array of Field objects
59
+ @fields_specs = @fields || []
60
+
61
+ @fields = []
62
+ @fields_hash = {}
63
+ @default_values ||= {}
64
+ @params = {}
65
+
66
+ @fields_specs.each do |field|
67
+ # create field here
68
+ add_field(field)
69
+ end
70
+
71
+ @errors = []
72
+ @field_errors = {}
73
+ end
74
+
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
78
+ end
79
+
80
+ # Creates and adds a field to the form, according to specified attributes.
81
+ def add_field(attrs)
82
+ raise "added second field named '#{attrs[:name]}'; field names must be unique" if @fields_hash[attrs[:name]]
83
+
84
+ attrs[:type] ||= :text
85
+ type = attrs[:type].to_sym
86
+ unless @@field_types.has_key?(type)
87
+ type = attrs[:type] = :text
88
+ end
89
+
90
+ @has_required_fields ||= attrs[:required]
91
+
92
+ field_class = @@field_types[type]
93
+ field = field_class.new(attrs)
94
+
95
+ field_name = field.name.to_s
96
+ field.form = self
97
+
98
+ unless type == :submit
99
+ @fields << field
100
+ @fields_hash[field_name] = field
101
+ end
102
+
103
+ @enctype = 'multipart/form-data' if field.type == :file
104
+
105
+ # if param submitted, set value of field to match
106
+ if (param = @params[field_name])
107
+ field.value = param
108
+ elsif @object && !attrs.has_key?(:value)
109
+ value = @object[field_name.to_sym] || @object[field_name]
110
+
111
+ case field.format
112
+ when :date:
113
+ field.value = value ? value.strftime("%m/%d/%Y") : ''
114
+ when :datetime:
115
+ field.value = value ? value.strftime("%m/%d/%Y %I:%M%p") : ''
116
+ else
117
+ field.value = value.to_s
118
+ end
119
+ end
120
+
121
+ field
122
+ end
123
+
124
+ # Creates and adds set of submit buttons to the form, per specified attributes.
125
+ def add_submit(attrs)
126
+ @submit = add_field({
127
+ :type => :submit,
128
+ :name => 'submit',
129
+ :save => false
130
+ }.merge(attrs) )
131
+ end
132
+
133
+ # Gets hash of form values ready to be saved to Sequel::Model object.
134
+ def sequel_values
135
+ @sequel_values ||= begin
136
+ hash = {}
137
+ @fields.each {|field| hash[field.name] = field.sequel_value unless field == :section_break }
138
+ hash
139
+ end
140
+ end
141
+
142
+ # Gets hash of form values.
143
+ def values
144
+ @values ||= begin
145
+ hash = {}
146
+ @fields.each {|field| hash[field.name] = field.value unless field == :section_break}
147
+ hash
148
+ end
149
+ end
150
+
151
+ # Add error message to be rendered with form.
152
+ # If multiple args given, first arg is field name, and second arg (error message)
153
+ # will render next to specified field.
154
+ def add_error(*args)
155
+ if args.size > 2
156
+ raise 'too many args'
157
+ elsif args.size == 0
158
+ raise 'at least one arg required'
159
+ end
160
+
161
+ # args.size == 1 or 2
162
+ if args.size == 1
163
+ if args[0].is_a?(String)
164
+ @errors << args[0]
165
+ return
166
+ else
167
+ field_name = args[0].field_name.to_s
168
+ message = args[0].message
169
+ end
170
+ else # args.size == 2
171
+ # args == [field_name, message]
172
+ field_name = args[0].to_s
173
+ message = args[1]
174
+ end
175
+ return @fields_hash[field_name].add_error(message)
176
+ end
177
+
178
+ # Validates form values against fields' format and required attributes,
179
+ # and returns values unless form has errors.
180
+ def validate
181
+ return nil unless submitted
182
+
183
+ @fields.each {|field| field.validate unless field == :section_break }
184
+
185
+ has_errors ? nil : values
186
+ end
187
+
188
+ # Saves form values to Sequel::Model object.
189
+ def save(object = @object)
190
+ @fields.each do |field|
191
+ next if field == :section_break
192
+ # ignore fields whose name starts with underscore
193
+ next if field.name =~ /\A\_/
194
+ # don't save 'ignore' fields to the database
195
+ next if field.ignore || !field.save
196
+
197
+ object[field.name.to_sym] = field.sequel_value
198
+ end
199
+
200
+ object.save
201
+ end
202
+
203
+ # Returns true if form has errors.
204
+ def has_errors
205
+ (@errors.size > 0 || @has_field_errors)
206
+ end
207
+
208
+ # Checks whether form was submitted and accepted by user and, if so,
209
+ # whether form validates.
210
+ # If form validates, saves form values to form's Sequel::Model object
211
+ # and returns true. Otherwise, returns false.
212
+ def process(object = @object)
213
+ return false unless submitted
214
+
215
+ if accepted
216
+ validate
217
+ return false if has_errors
218
+
219
+ save(object)
220
+ end
221
+
222
+ return true
223
+ end
224
+
225
+ # Returns true if user submitted form with non-cancel submit button
226
+ # (non-nil submit param, not equal to cancel submit button value).
227
+ def accepted
228
+ raise 'form missing submit field' unless @submit
229
+ return params[@submit.name] != @submit.cancel
230
+ end
231
+
232
+ # Renders error HTML block for top of form, and returns as string.
233
+ def errors_html
234
+ return nil unless has_errors
235
+
236
+ @errors << "Please remedy the errors shown below." if @has_field_errors
237
+
238
+ if @errors.size == 1
239
+ content = @errors[0]
240
+ else
241
+ content = "<ul>" + @errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
242
+ end
243
+
244
+ @errors.pop if @has_field_errors
245
+
246
+ plural = @errors.size > 1 || @has_field_errors ? 's' : nil
247
+ %Q(<table border=0 cellspacing=0><tr class="kiss_error"><th>Error#{plural}</th></td><td class="kiss_required"></td><td>#{content}</td></tr></table>)
248
+ end
249
+
250
+ # Renders current action using form's HTML as action render content.
251
+ def render(options = {})
252
+ @request.render options.merge(:content => html)
253
+ end
254
+
255
+ # Renders beginning of form (form open tag and form/field/error styles).
256
+ def html_open
257
+ @error_class ||= 'kiss_form_error_message'
258
+ @field_error_class ||= @error_class
259
+
260
+ # form tag
261
+ form_attrs = ['method','enctype','class','style'].map do |attr|
262
+ "#{attr}=\"#{send attr}\""
263
+ end.join(' ')
264
+ form_tag = %Q(<form action="#{@action}" #{form_attrs}><input type=hidden name="form" value="#{@name}">)
265
+
266
+ # style tag
267
+ styles = []
268
+ styles.push( <<-EOT
269
+ .kiss_form table {
270
+ margin-bottom: 6px;
271
+ }
272
+ .kiss_form td {
273
+ padding: 2px 4px;
274
+ }
275
+ .kiss_form tr.kiss_error {
276
+ background-color: #ff8;
277
+ }
278
+ .kiss_form tr.kiss_error th {
279
+ vertical-align: top;
280
+ padding: 2px 4px 3px 4px;
281
+ text-align: right;
282
+ background-color: #fc7;
283
+ color: #900;
284
+ border: 1px solid #f96;
285
+ border-right: none;
286
+ }
287
+ .kiss_form tr.kiss_error td {
288
+ vertical-align: top;
289
+ padding: 2px 6px 3px 6px;
290
+ border: 1px solid #f96;
291
+ border-left: none;
292
+ color: #000;
293
+ }
294
+ .kiss_form tr.kiss_error ul {
295
+ padding-left: 16px;
296
+ margin: 0;
297
+ }
298
+ .kiss_form tr.kiss_error td.kiss_required {
299
+ padding: 0;
300
+ border-right: none;
301
+ }
302
+ .kiss_form .kiss_help {
303
+ padding: 0px 3px 8px 6px;
304
+ font-size: 90%;
305
+ color: #555;
306
+ }
307
+ .kiss_form td.kiss_required {
308
+ padding: 2px 0px;
309
+ color: #c00;
310
+ }
311
+ .kiss_form td.kiss_label {
312
+ text-align: right;
313
+ white-space: nowrap;
314
+ }
315
+ .kiss_form tr.kiss_prompt td {
316
+ padding: 8px 3px 2px 4px;
317
+ }
318
+ .kiss_form tr.kiss_submit td.kiss_submit {
319
+ padding: 6px 3px;
320
+ }
321
+ .kiss_form input[type="text"],.kiss_form textarea {
322
+ width: 250px;
323
+ }
324
+ EOT
325
+ )
326
+ styles.push( <<-EOT
327
+ .kiss_form_error_message {
328
+ padding: 1px 4px 2px 4px;
329
+ border: 1px solid #f96;
330
+ background-color: #ff8;
331
+ color: #900;
332
+ font-size: 85%;
333
+ display: block;
334
+ float: left;
335
+ margin-top: 1px;
336
+
337
+ }
338
+ .kiss_form_error_message div {
339
+ color: #c00;
340
+ font-weight: bold;
341
+ }
342
+ .kiss_form_error_message ul {
343
+ padding-left: 16px;
344
+ margin: 0;
345
+ }
346
+ EOT
347
+ ) if @error_class == 'kiss_form_error_message'
348
+ style_tag = styles.size == 0 ? '' : "<style>" + styles.join('') + "</style>"
349
+
350
+ # combine
351
+ return %Q(#{form_tag}#{style_tag}<div class="kiss_form">#{errors_html})
352
+ end
353
+
354
+ # Renders end of form (form close tag).
355
+ def html_close
356
+ '</div></form>'
357
+ end
358
+
359
+ # Renders form fields HTML.
360
+ def fields_html
361
+ @fields.map do |field|
362
+ field == :section_break ? begin
363
+ table_html_close + table_html_open
364
+ end : field_html(field)
365
+ end.join
366
+ end
367
+
368
+ # Renders open of form table.
369
+ def table_html_open
370
+ %Q(<table border=0 cellspacing=0>) +
371
+ (@has_required_fields && @mark_required ? %Q( <tr><td></td><td class="kiss_required">*</td><td colspan=2 class="kiss_help">Required field.</td></tr> ) : '')
372
+ end
373
+
374
+ # Renders close of form table.
375
+ def table_html_close
376
+ '</table>'
377
+ end
378
+
379
+ # Renders form submit buttons.
380
+ def submit_html
381
+ field_html(@submit)
382
+ end
383
+
384
+ # Renders complete form HTML.
385
+ def html
386
+ return [
387
+ html_open,
388
+ table_html_open,
389
+ fields_html,
390
+ submit_html,
391
+ table_html_close,
392
+ html_close
393
+ ].flatten.join
394
+ end
395
+
396
+ # Renders HTML for specified form field.
397
+ def field_html(field)
398
+ type = field.type
399
+ prompt = field.prompt.to_s
400
+ label = field.label.to_s
401
+
402
+ [
403
+ (prompt != '' ? %Q(<tr class="kiss_prompt"><td colspan=2></td><td colspan=2>#{prompt}</td></tr>) : ''),
404
+ "",
405
+ %Q(<tr class="kiss_#{type}"><td class="kiss_label">),
406
+ label != '' && prompt == '' ? label + ':' : '',
407
+ '</td><td class="kiss_required">',
408
+ field.required ? @mark_required : '',
409
+ %Q(</td><td class="kiss_#{type}">),
410
+ field.html, "</td></tr>"
411
+ ].join
412
+ end
413
+ end
414
+ end
@@ -0,0 +1,80 @@
1
+ class Kiss
2
+ # This class leftover from a previous version of Kiss and is not currently used.
3
+ # In fact, this file is not required from any other part of Kiss.
4
+ #
5
+ # But we're keeping it around in case it's useful for the anticipated rewrite of
6
+ # Kiss.validate_format.
7
+ class Format
8
+ def self.regexp
9
+ @@regexp
10
+ end
11
+ def self.error
12
+ @@error
13
+ end
14
+
15
+ class Integer < Kiss::Format
16
+ @regexp = /\A\-?\d+\Z/
17
+ @error = 'must be an integer'
18
+ end
19
+
20
+ class PositiveInteger < Kiss::Format
21
+ @regexp = /\A\d*[1-9]\d*\Z/
22
+ @error = 'must be a positive integer'
23
+ end
24
+
25
+ class UnsignedInteger < Kiss::Format
26
+ @regexp = /\A\d+\Z/
27
+ @error = 'must be a positive integer or zero'
28
+ end
29
+
30
+ class NegativeInteger < Kiss::Format
31
+ @regexp = /\A\-\d*[1-9]\d*\Z/
32
+ @error = 'must be a negative integer'
33
+ end
34
+
35
+ class AlphaNum
36
+ @regexp = /\A[a-z0-9]\Z/i
37
+ @error = 'allows only letters and numbers'
38
+ end
39
+
40
+ class Word
41
+ @regexp = /\A\w+\Z/
42
+ @error = 'allows only letters, numbers, and _'
43
+ end
44
+
45
+ class EmailAddress
46
+ @regexp = /\A[A-Z0-9._%+-]+\@([A-Z0-9-]+\.)+[A-Z]{2,4}\Z/i
47
+ @error = 'must be a valid email address'
48
+ end
49
+
50
+ class Date
51
+ @regexp = /\A\d+\D\d+(\D\d+)?\Z/
52
+ @error = 'must be a valid date'
53
+ end
54
+
55
+
56
+ @@classes = {
57
+ :integer => Kiss::Format::Integer,
58
+
59
+ :integer_positive => Kiss::Format::PositiveInteger,
60
+ :positive_integer => Kiss::Format::PositiveInteger,
61
+ :id => Kiss::Format::PositiveInteger,
62
+
63
+ :integer_unsigned => Kiss::Format::UnsignedInteger,
64
+ :unsigned_integer => Kiss::Format::UnsignedInteger,
65
+ :id_or_zero => Kiss::Format::UnsignedInteger,
66
+ :id_zero => Kiss::Format::UnsignedInteger,
67
+
68
+ :integer_negative => Kiss::Format::NegativeInteger,
69
+ :negative_integer => Kiss::Format::NegativeInteger,
70
+
71
+ :word => Kiss::Format::Word,
72
+ :alphanum => Kiss::Format::AlphaNum,
73
+ :email_address => Kiss::Format::EmailAddress,
74
+ :date => Kiss::Format::Date
75
+ }
76
+ def self.from_symbol(symbol)
77
+ return @@classes[symbol] || (raise "no format for #{symbol}")
78
+ end
79
+ end
80
+ end
data/lib/kiss/hacks.rb ADDED
@@ -0,0 +1,140 @@
1
+ # This is a collection of hacks to enable various Ruby lnaguage enhancements
2
+ # and other features of Kiss.
3
+
4
+ # Placeholder; overloaded by Kiss Rack builder option ShowDebug.
5
+ def debug(*args); end
6
+ # Placeholder; overloaded by Kiss Rack builder option Bench.
7
+ def bench(*args); end
8
+ # Placeholder; to be overloaded by Kiss Rack builder option MultiBench
9
+ # (not yet implemented).
10
+ def multibench(*args); end
11
+
12
+ class Kiss
13
+ # Used when Kiss#file_cache called from Kiss::Action#file_cache.
14
+ # Caught by Kiss's Rack builder option FileNotFound.
15
+ class TemplateFileNotFound < LoadError; end
16
+ class FileNotFound < LoadError; end
17
+ end
18
+
19
+
20
+ # Methods to enable hash.attr == hash[attr] syntax,
21
+ # including hash.id and hash.type
22
+ class Hash
23
+ def method_missing(meth,*args)
24
+ if /=$/=~(meth=meth.id2name) then
25
+ self[meth[0...-1]] = (args.length<2 ? args[0] : args)
26
+ else
27
+ self[meth] || self[meth.to_sym]
28
+ end
29
+ end
30
+ def id
31
+ self['id'] || self [:id]
32
+ end
33
+ def type
34
+ self['type'] || self [:type] || Hash
35
+ end
36
+ end
37
+
38
+ # Enables FixNum conversion to time duration values (in seconds).
39
+ class Fixnum
40
+ # 2.weeks = 14.days
41
+ def weeks
42
+ self * 7.days
43
+ end
44
+ # 1.days = 24.hours
45
+ def days
46
+ self * 24.hours
47
+ end
48
+ # 1.hours = 60.minutes = 3600 (seconds)
49
+ def hours
50
+ self * 60.minutes
51
+ end
52
+ # 1.minutes = 60 (seconds)
53
+ # 5.minutes = 300 (seconds)
54
+ def minutes
55
+ self * 60
56
+ end
57
+ # 5.minutes.ago = Time.now - 300
58
+ def ago
59
+ Time.now - self
60
+ end
61
+ end
62
+
63
+ class Date
64
+ # Returns string representing date in m/d/yyyy format
65
+ def mdy
66
+ sprintf('%d/%d/%04d',month,mday,year)
67
+ end
68
+ end
69
+
70
+ class NilClass
71
+ def mdy
72
+ ''
73
+ end
74
+ def size
75
+ 0
76
+ end
77
+ def length
78
+ 0
79
+ end
80
+ end
81
+
82
+ # Corrections to sequel_core/core_sql.rb
83
+ class String
84
+ def to_sequel_time
85
+ self == '0000-00-00 00:00:00' ? nil : begin
86
+ Time.parse(self)
87
+ rescue Exception => e
88
+ raise Sequel::Error::InvalidValue, "Invalid time value '#{self}' (#{e.message})"
89
+ end
90
+ end
91
+
92
+ # Converts a string into a Date object.
93
+ def to_date
94
+ begin
95
+ Date.parse(self)
96
+ rescue Exception => e
97
+ if (self == '0000-00-00')
98
+ nil
99
+ else
100
+ raise Sequel::Error::InvalidValue, "Invalid date value '#{self}' (#{e.message})"
101
+ end
102
+ end
103
+ end
104
+
105
+ def to_const
106
+ begin
107
+ parts = self.split(/::/)
108
+ klass = Kernel
109
+ while (next_part = parts.shift)
110
+ klass = klass.const_get(next_part)
111
+ end
112
+
113
+ klass
114
+ rescue
115
+ raise "Constant '#{self}' not defined"
116
+ end
117
+ end
118
+ end
119
+
120
+ class Symbol
121
+ def to_const
122
+ self.to_s.to_const
123
+ end
124
+ end
125
+
126
+ module Rack
127
+ class Request
128
+ def server
129
+ url = scheme + "://"
130
+ url << host
131
+
132
+ if scheme == "https" && port != 443 ||
133
+ scheme == "http" && port != 80
134
+ url << ":#{port}"
135
+ end
136
+
137
+ url
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,56 @@
1
+ class Kiss
2
+ # Enables `loop' features for certain template loops (while, for...in).
3
+ # Similar to features of Perl's Template::Iterator by Andy Wardley.
4
+ class Iterator
5
+ attr_reader :index, :collection
6
+
7
+ # Creates a new loop iterator.
8
+ def initialize(collection = nil)
9
+ @collection = collection
10
+ @index = -1
11
+ end
12
+
13
+ # Used by template erubis pre-processing voodoo to advance
14
+ # the iterator index. Not intended for any other use.
15
+ def increment
16
+ @index = @index + 1
17
+ end
18
+
19
+ # Return current iteration number, indexed from one instead of zero.
20
+ def count
21
+ @index + 1
22
+ end
23
+
24
+ # Returns true if this is loop's first iteration.
25
+ def first
26
+ @index == 0
27
+ end
28
+
29
+ # Returns true if this is the last iteration of a
30
+ # loop over a collection.
31
+ def last
32
+ count == size
33
+ end
34
+
35
+ # Return previous item in loop-iterated collection.
36
+ def prev
37
+ @collection ? @collection[index-1] : nil
38
+ end
39
+
40
+ # Return next item in loop-iterated collection.
41
+ def next
42
+ @collection ? @collection[index+1] : nil
43
+ end
44
+
45
+ # Returns index of loop's last iteration.
46
+ def max
47
+ @collection ? @collection.size-1 : nil
48
+ end
49
+
50
+ # Returns number of iterations in loop over a collection
51
+ # (size of the iterated collection).
52
+ def size
53
+ @collection ? @collection.size : nil
54
+ end
55
+ end
56
+ end