kiss 0.9

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