kiss 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,359 @@
1
+ class Kiss
2
+ # Generates HTML exception reports for Rack::ShowExceptions and
3
+ # Rack::LogExceptions.
4
+ class ExceptionReport
5
+ @@context = 7
6
+
7
+ class << self
8
+ def generate(env, exception)
9
+ req = Rack::Request.new(env)
10
+ path = (req.script_name + req.path_info).squeeze("/")
11
+ url = req.scheme + '://' + req.host + path
12
+
13
+ backtrace = exception.backtrace
14
+ #backtrace.shift while lines[0] =~ /\/lib\/kiss(\/|\.rb)/
15
+
16
+ frames = backtrace.map { |line|
17
+ frame = {}
18
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
19
+ frame.filename = $1
20
+ frame.lineno = $2.to_i
21
+ frame.function = $4
22
+ lines = nil
23
+
24
+ begin
25
+ lineno = frame.lineno-1
26
+ lines ||= ::File.readlines(frame.filename)
27
+ frame.pre_context_lineno = [lineno-@@context, 0].max
28
+ frame.pre_context = lines[frame.pre_context_lineno...lineno]
29
+ frame.context_line = lines[lineno].chomp
30
+ frame.post_context_lineno = [lineno+@@context, lines.size].min
31
+ frame.post_context = lines[lineno+1..frame.post_context_lineno]
32
+ rescue
33
+ end
34
+
35
+ frame
36
+ else
37
+ nil
38
+ end
39
+ }.compact
40
+
41
+ env["rack.errors"].puts "#{exception.class}: #{exception.message}"
42
+ env["rack.errors"].puts exception.backtrace.map { |l| "\t" + l }
43
+ env["rack.errors"].flush
44
+
45
+ db_query = begin (Sequel::MySQL::Database.last_query) rescue nil end
46
+
47
+ @@erubis ||= Erubis::Eruby.new(template)
48
+ @@erubis.result(binding)
49
+ end
50
+
51
+ def absolute_path(filename)
52
+ filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
53
+ end
54
+
55
+ def textmate_href(frame)
56
+ "txmt://open?url=file://"+(h(absolute_path(frame.filename)))+"&amp;line="+(h frame.lineno)
57
+ end
58
+
59
+ def h(obj) # :nodoc:
60
+ case obj
61
+ when String
62
+ Rack::Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
63
+ else
64
+ Rack::Utils.escape_html(obj.inspect)
65
+ end
66
+ end
67
+
68
+ # :stopdoc:
69
+
70
+ # adapted from Django <djangoproject.com>
71
+ # Copyright (c) 2005, the Lawrence Journal-World
72
+ # Used under the modified BSD license:
73
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
74
+ def template
75
+ <<-EOT
76
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
77
+ <html lang="en">
78
+ <head>
79
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
80
+ <meta name="robots" content="NONE,NOARCHIVE" />
81
+ <title>Exception: <%=h exception.class %> - <%=h url %></title>
82
+ <style type="text/css">
83
+ html * { padding:0; margin:0; }
84
+ body *, .body * { padding:10px 20px; }
85
+ body * *, .body * * { padding:0; }
86
+ body { font:small sans-serif; }
87
+ body>div { border-bottom:1px solid #ddd; }
88
+ h1 { font-weight:normal; font-size: 18px; font-style: italic; color: #d96 }
89
+ h2 { margin-bottom:.8em; }
90
+ h2 span { font-size:80%; color:#000; font-weight:normal; }
91
+ h3 { margin:1em 0 .5em 0; }
92
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
93
+ table {
94
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
95
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
96
+ thead th {
97
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
98
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
99
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
100
+ table.vars { margin:5px 0 2px 40px; }
101
+ table.vars td, table.req td { font-family:monospace; }
102
+ table td.code { width:100%;}
103
+ table td.code div { overflow:hidden; }
104
+ table.source th { color:#666; }
105
+ table.source td {
106
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
107
+ ul.traceback { list-style-type:none; }
108
+ ul.traceback li.frame { margin-bottom:1em; }
109
+ div.context { margin: 10px 0 10px 40px; background-color: #fff; }
110
+ div.context ol {
111
+ padding: 1px; margin-bottom: -1px; }
112
+ div.context ol li {
113
+ font-family:monospace; color:#666; cursor:pointer; padding: 0 2px; }
114
+ div.context ol.context-line li { color:black; background-color:#eca; }
115
+ div.context ol.context-line li span { float: right; }
116
+ div.commands { margin-left: 40px; }
117
+ div.commands a { color:black; text-decoration:none; }
118
+ #summary { background: #ffd; }
119
+ #summary h2 { font-weight: normal; color: #a10; font-size: 14px; font-weight: bold }
120
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
121
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }
122
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
123
+ #explanation { background:#eee; }
124
+ #template, #template-not-exist { background:#f6f6f6; }
125
+ #template-not-exist ul { margin: 0 0 0 20px; }
126
+ #traceback { background:#eee; }
127
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
128
+ #summary table { border:none; background:transparent; }
129
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
130
+ #requestinfo h3 { margin-bottom:-1em; }
131
+ .error { background: #ffc; }
132
+ .specific { color:#cc3300; font-weight:bold; }
133
+ </style>
134
+ <script type="text/javascript">
135
+ //<!--
136
+ function getElementsByClassName(oElm, strTagName, strClassName){
137
+ // Written by Jonathan Snook, http://www.snook.ca/jon;
138
+ // Add-ons by Robert Nyman, http://www.robertnyman.com
139
+ var arrElements = (strTagName == "*" && document.all)? document.all :
140
+ oElm.getElementsByTagName(strTagName);
141
+ var arrReturnElements = new Array();
142
+ strClassName = strClassName.replace(/\-/g, "\\-");
143
+ var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
144
+ var oElement;
145
+ for(var i=0; i<arrElements.length; i++){
146
+ oElement = arrElements[i];
147
+ if(oRegExp.test(oElement.className)){
148
+ arrReturnElements.push(oElement);
149
+ }
150
+ }
151
+ return (arrReturnElements)
152
+ }
153
+ function hideAll(elems) {
154
+ for (var e = 0; e < elems.length; e++) {
155
+ elems[e].style.display = 'none';
156
+ }
157
+ }
158
+ window.onload = function() {
159
+ hideAll(getElementsByClassName(document, 'table', 'vars'));
160
+ hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
161
+ hideAll(getElementsByClassName(document, 'ol', 'post-context'));
162
+ toggle('pre<%=h frames.first.object_id %>', 'post<%=h frames.first.object_id %>')
163
+ }
164
+ function toggle() {
165
+ for (var i = 0; i < arguments.length; i++) {
166
+ var e = document.getElementById(arguments[i]);
167
+ if (e) {
168
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
169
+ }
170
+ }
171
+ return false;
172
+ }
173
+
174
+ function varToggle(link, id) {
175
+ toggle('v' + id);
176
+ var s = link.getElementsByTagName('span')[0];
177
+ var uarr = String.fromCharCode(0x25b6);
178
+ var darr = String.fromCharCode(0x25bc);
179
+ s.innerHTML = s.innerHTML == uarr ? darr : uarr;
180
+ return false;
181
+ }
182
+ //-->
183
+ </script>
184
+ </head>
185
+ <body>
186
+
187
+ <div id="summary">
188
+ <a name="summary"></a>
189
+ <h1><%=h exception.class %></h1>
190
+ <h2><code><%=(h exception.message).gsub(/\n/,'<br>') %></code></h2>
191
+ <table><tr>
192
+ <th>Ruby</th>
193
+ <td><code><%=h frames.first.filename %></code>: in <code><%=h frames.first.function %></code>, <a href="<%= textmate_href(frames.first) %>">line <%=h frames.first.lineno %></a></td>
194
+ </tr><tr>
195
+ <th>Web</th>
196
+ <td><code><%=h req.request_method %> <%=h(url)%></code></td>
197
+ </tr></table>
198
+
199
+ <h3>Jump to:</h3>
200
+ <ul id="quicklinks">
201
+ <li><a href="#cache">Cache</a></li>
202
+ <li><a href="#db-query">DB Query</a></li>
203
+ <li><a href="#get-info">GET</a></li>
204
+ <li><a href="#post-info">POST</a></li>
205
+ <li><a href="#cookie-info">Cookies</a></li>
206
+ <li><a href="#env-info">ENV</a></li>
207
+ </ul>
208
+ </div>
209
+
210
+ <div id="traceback">
211
+ <h2>Traceback <span>(innermost first)</span></h2>
212
+ <ul class="traceback">
213
+ <% frames.each { |frame| %>
214
+ <li class="frame">
215
+ <a href="<%= textmate_href(frame) %>">line <%= h frame.lineno %></a> of <code><%=h frame.filename %></code> (in <code><b><%=h frame.function %></b></code>)
216
+
217
+ <% if frame.context_line %>
218
+ <div class="context" id="c<%=h frame.object_id %>">
219
+ <% if frame.pre_context %>
220
+ <ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
221
+ <% frame.pre_context.each { |line| %>
222
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
223
+ <% } %>
224
+ </ol>
225
+ <% end %>
226
+
227
+ <ol start="<%=h frame.lineno %>" class="context-line">
228
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %></li></ol>
229
+
230
+ <% if frame.post_context %>
231
+ <ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
232
+ <% frame.post_context.each { |line| %>
233
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
234
+ <% } %>
235
+ </ol>
236
+ <% end %>
237
+ </div>
238
+ <% end %>
239
+ </li>
240
+ <% } %>
241
+ </ul>
242
+ </div>
243
+
244
+ <div id="requestinfo">
245
+ <h2>Additional information</h2>
246
+
247
+ <a name="cache"></a>
248
+ <h3 id="cache">Cache</h3>
249
+ <p><code><%=h Kiss.exception_cache.inspect %></code></p>
250
+
251
+ <a name="db-query"></a>
252
+ <h3 id="db-query">DB Query</h3>
253
+ <p><% if db_query %><code><%=(h db_query).gsub(/\n/,'<br/>') %></code><% else %>No database queries executed.<% end %></p>
254
+
255
+ <a name="get-info"></a>
256
+ <h3 id="get-info">GET</h3>
257
+ <% unless req.GET.empty? %>
258
+ <table class="req">
259
+ <thead>
260
+ <tr>
261
+ <th>Variable</th>
262
+ <th>Value</th>
263
+ </tr>
264
+ </thead>
265
+ <tbody>
266
+ <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
267
+ <tr>
268
+ <td><%=h key %></td>
269
+ <td class="code"><div><%=h val.inspect %></div></td>
270
+ </tr>
271
+ <% } %>
272
+ </tbody>
273
+ </table>
274
+ <% else %>
275
+ <p>No GET data.</p>
276
+ <% end %>
277
+
278
+ <a name="post-info"></a>
279
+ <h3 id="post-info">POST</h3>
280
+ <% unless req.POST.empty? %>
281
+ <table class="req">
282
+ <thead>
283
+ <tr>
284
+ <th>Variable</th>
285
+ <th>Value</th>
286
+ </tr>
287
+ </thead>
288
+ <tbody>
289
+ <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
290
+ <tr>
291
+ <td><%=h key %></td>
292
+ <td class="code"><div><%=h val.inspect %></div></td>
293
+ </tr>
294
+ <% } %>
295
+ </tbody>
296
+ </table>
297
+ <% else %>
298
+ <p>No POST data.</p>
299
+ <% end %>
300
+
301
+ <a name="cookie-info"></a>
302
+ <h3 id="cookie-info">Cookies</h3>
303
+ <% unless req.cookies.empty? %>
304
+ <table class="req">
305
+ <thead>
306
+ <tr>
307
+ <th>Variable</th>
308
+ <th>Value</th>
309
+ </tr>
310
+ </thead>
311
+ <tbody>
312
+ <% req.cookies.each { |key, val| %>
313
+ <tr>
314
+ <td><%=h key %></td>
315
+ <td class="code"><div><%=h val.inspect %></div></td>
316
+ </tr>
317
+ <% } %>
318
+ </tbody>
319
+ </table>
320
+ <% else %>
321
+ <p>No cookie data.</p>
322
+ <% end %>
323
+
324
+ <a name="env-info"></a>
325
+ <h3 id="env-info">Rack ENV</h3>
326
+ <table class="req">
327
+ <thead>
328
+ <tr>
329
+ <th>Variable</th>
330
+ <th>Value</th>
331
+ </tr>
332
+ </thead>
333
+ <tbody>
334
+ <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
335
+ <tr>
336
+ <td><%=h key %></td>
337
+ <td class="code"><div><%=h val %></div></td>
338
+ </tr>
339
+ <% } %>
340
+ </tbody>
341
+ </table>
342
+
343
+ </div>
344
+
345
+ <div id="explanation">
346
+ <p>
347
+ Generated by Kiss::ExceptionReport.
348
+ </p>
349
+ </div>
350
+
351
+ </body>
352
+ </html>
353
+ EOT
354
+ end
355
+
356
+ # :startdoc:
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,296 @@
1
+ class Kiss
2
+ class Form
3
+ class Field
4
+ attr_accessor :name,:type,:form,:format,:currency,:label,:no_label,:prompt,:value,:ignore,:save,
5
+ :options,:options_value_key,:options_display_key,:required,:cancel,:columns,:style,:hidden_join
6
+
7
+ include Kiss::Form::AttributesSetter
8
+
9
+ def initialize(attrs)
10
+ @save = true
11
+ @value = ''
12
+
13
+ set_attributes(attrs,[:name,:type])
14
+
15
+ @errors = []
16
+ end
17
+
18
+ def column_layout(elements_html)
19
+ if @columns
20
+ # format elements into cells
21
+ elements_html.map! { |el| "<td>" + el + "</td>" }
22
+
23
+ # compute height = ceiling(number of elements / columns)
24
+ height = ((elements_html.size + columns - 1)/columns).to_i
25
+
26
+ # group cells into columns
27
+ element_ranges = (1..height).to_a.map {|i| ((i-1)*@columns..(i*@columns-1))}
28
+ return ['<table class="kiss_field_columns">',element_ranges.map do |range|
29
+ ["<tr>", elements_html[range], "</tr>"]
30
+ end,'</table>'].flatten.join
31
+ join_with = ''
32
+ else
33
+ join_with = '&nbsp; '
34
+ end
35
+
36
+ # else columns <= 1
37
+ elements_html.join(join_with)
38
+ end
39
+
40
+ def sequel_value
41
+ case @format
42
+ when :date:
43
+ return Kiss.mdy_to_ymd(value)
44
+ when :datetime:
45
+ datetime = value.sub(/([ap]m)\s*\Z/i,' \1')
46
+ date, time, ampm = datetime.split(/\s+/)
47
+
48
+ hours, minutes = time.split(/:/)
49
+ hours = 0 if hours.to_i == 12 && ampm
50
+ if (ampm.downcase == 'pm')
51
+ hours = hours.to_i + 12
52
+ end
53
+
54
+ return Kiss.mdy_to_ymd(date) + " #{hours}:#{minutes}"
55
+ else
56
+ return value.is_a?(Array) ? value.map {|v| v.gsub(/,/,'\,')}.join(',') : value
57
+ end
58
+ end
59
+
60
+ def value
61
+ @value ||= @form.params[@name]
62
+ end
63
+
64
+ def add_error(message)
65
+ @errors << message
66
+ @form.has_field_errors = true
67
+ end
68
+
69
+ def validate
70
+ if (error = Kiss.validate_value(value,@format,@required))
71
+ add_error("#{@label} #{error}")
72
+ end
73
+
74
+ value
75
+ end
76
+
77
+ def errors_html
78
+ return nil unless @errors.size > 0
79
+
80
+ if @errors.size == 1
81
+ content = @errors[0]
82
+ else
83
+ content = "<ul>" + @errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
84
+ end
85
+
86
+ %Q(<span class="#{@form.field_error_class}">#{content}</span><br clear="all" />)
87
+ end
88
+
89
+ def element_html
90
+ if @currency == :dollars
91
+ return '$' + input_tag_html( :value => @value, :style => 'width: 80px' )
92
+ end
93
+ input_tag_html( :value => @value )
94
+ end
95
+
96
+ def html
97
+ errors = errors_html
98
+ element_html + (errors ? %Q(<br/>#{errors}) : '')
99
+ end
100
+
101
+ def input_tag_html(attrs = {}, extra_html = '')
102
+ attrs = attrs.clone
103
+ attrs[:name] ||= @name
104
+ attrs[:type] ||= @type
105
+ attrs[:style] ||= @style
106
+
107
+ attrs_html = []
108
+ attrs.each_pair {|k,v| attrs_html.push %Q(#{k}="#{v}") }
109
+ [ '<input', attrs_html, extra_html + '/>' ].flatten.join(' ')
110
+ end
111
+ end
112
+
113
+ class HiddenField < Field
114
+ end
115
+ class TextField < Field
116
+ end
117
+
118
+ class TextAreaField < Field
119
+ attr_accessor :rows,:cols
120
+
121
+ def initialize(*args)
122
+ @rows = 5
123
+ @cols = 20
124
+ super(*args)
125
+ end
126
+ def element_html
127
+ %Q(<textarea name="#{@name}" rows="#{@rows ||= 1}" cols="#{@cols ||= 1}" style="#{@style ||= ''}">#{@value}</textarea>)
128
+ end
129
+ end
130
+
131
+ class PasswordField < Field
132
+ def element_html
133
+ input_tag_html
134
+ end
135
+ end
136
+
137
+ class BooleanField < Field
138
+ def element_html
139
+ input_tag_html({ :value => 1 }, @value ? 'CHECKED ' : '')
140
+ end
141
+ end
142
+
143
+ class FileField < Field
144
+ def element_html
145
+ input_tag_html
146
+ end
147
+
148
+ def get_file_name
149
+
150
+ end
151
+
152
+ def get_file_data
153
+
154
+ end
155
+ end
156
+
157
+ class SubmitField < Field
158
+ def initialize(*args)
159
+ @save = false
160
+ super(*args)
161
+ end
162
+
163
+ def element_html
164
+ elements_html.join(' ')
165
+ end
166
+
167
+ def validate
168
+ # do nothing for now
169
+ value
170
+ end
171
+
172
+ def elements_html
173
+ @options.map do |option|
174
+ input_tag_html({ :value => option })
175
+ end
176
+ end
177
+ end
178
+
179
+ # ------ MultiChoiceField
180
+
181
+ class MultiChoiceField < Field
182
+ def option_pairs
183
+ if (defined? @options_value_key)
184
+ # options_display_map not supported
185
+ # default to options_display_key
186
+ return @options.map {|option| [ option[@options_value_key], option[@options_display_key] ]}
187
+ end
188
+
189
+ @options.map {|option| [ option, option ] }
190
+ end
191
+
192
+ def matched_options(value)
193
+ value = [value].flatten
194
+ @options_value_key ?
195
+ @options.select {|o| value.select {|v| o[@options_value_key] == v } }.flatten :
196
+ @options.select {|o| value.select {|v| o == v } }.flatten
197
+ end
198
+
199
+ def validate
200
+ @value = @form.params[@name]
201
+
202
+ if (@value =~ /\S/)
203
+ add_error "#{@label} invalid" unless matched_options(@value).size > 0
204
+ elsif @required
205
+ add_error "#{@label} required"
206
+ end
207
+ end
208
+ end
209
+
210
+ class SelectField < MultiChoiceField
211
+ def element_html
212
+ return 'No options' unless @options.size > 0
213
+
214
+ placeholder_html = %Q(<option value="">Choose Here</option>)
215
+
216
+ options_html = option_pairs.map do |value,display|
217
+ selected = (@value.to_s == value.to_s) ? ' selected' : ''
218
+ %Q(<option value="#{value}"#{selected}>#{display}</option>)
219
+ end
220
+
221
+ [%Q(<select name="#{@name}" style="#{@style}">), placeholder_html, options_html, '</select>'].flatten.join
222
+ end
223
+ end
224
+
225
+ class RadioField < MultiChoiceField
226
+ def element_html
227
+ column_layout(elements_html)
228
+ end
229
+
230
+ def elements_html
231
+ return option_pairs.map do |value,display|
232
+ input_tag_html({ :value => value }, (@value.to_s == value.to_s) ? ' checked' : '' ) + display
233
+ end
234
+ end
235
+ end
236
+
237
+
238
+ # ------ MultiValueField
239
+
240
+ class MultiValueField < MultiChoiceField
241
+ def validate
242
+ @value = @form.params[@name.to_s+'[]'] || []
243
+
244
+ if @value.size > 0
245
+ @value.each do |value|
246
+ unless (matched_options(value).size > 0)
247
+ add_error "Invalid selection for #{@label}"
248
+ return
249
+ end
250
+ end
251
+ elsif @required
252
+ add_error "#{@label} required"
253
+ end
254
+ end
255
+
256
+ def selected_option_values
257
+ @selected_option_values ||= @value ? Hash[ *(@value.map {|v| [v.to_s,true]}.flatten) ] : {}
258
+ end
259
+ end
260
+
261
+ class CheckboxField < MultiValueField
262
+ def element_html
263
+ if @hidden_join
264
+ hidden_options = input_tag_html(
265
+ :type => 'hidden',
266
+ :name => "#{@name}_options",
267
+ :value => option_pairs.map {|v,d| v}.join(@hidden_join)
268
+ )
269
+ else
270
+ hidden_options = ''
271
+ end
272
+ column_layout(elements_html) + hidden_options
273
+ end
274
+
275
+ def elements_html
276
+ return option_pairs.map do |value,display|
277
+ input_tag_html({ :name => @name.to_s+'[]', :value => value }, selected_option_values[value.to_s] ? ' checked' : '' ) + display.to_s
278
+ end
279
+ end
280
+ end
281
+
282
+ class MultiSelectField < MultiValueField
283
+ def element_html
284
+ options_html = option_pairs.map do |value,display|
285
+ selected = selected_option_values[value] ? ' selected' : ''
286
+ %Q(<option value="#{value}"#{selected}>#{display}</option>)
287
+ end
288
+
289
+ [%Q(<select multiple name="#{@name}[]" size="#{@size}" style="#{@style}">), options_html, '</select>'].flatten.join
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ # not implemented yet:
296
+ # :multitext => MultiTextField,