kiss 0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/lib/kiss/action.rb +204 -0
- data/lib/kiss/controller_accessors.rb +101 -0
- data/lib/kiss/exception_report.rb +359 -0
- data/lib/kiss/form/field.rb +296 -0
- data/lib/kiss/form.rb +414 -0
- data/lib/kiss/format.rb +80 -0
- data/lib/kiss/hacks.rb +140 -0
- data/lib/kiss/iterator.rb +56 -0
- data/lib/kiss/mailer.rb +92 -0
- data/lib/kiss/model.rb +114 -0
- data/lib/kiss/rack/bench.rb +131 -0
- data/lib/kiss/rack/email_errors.rb +64 -0
- data/lib/kiss/rack/facebook.rb +28 -0
- data/lib/kiss/rack/file_not_found.rb +42 -0
- data/lib/kiss/rack/log_exceptions.rb +23 -0
- data/lib/kiss/rack/show_debug.rb +82 -0
- data/lib/kiss/rack/show_exceptions.rb +27 -0
- data/lib/kiss/sequel_mysql.rb +23 -0
- data/lib/kiss/sequel_session.rb +166 -0
- data/lib/kiss/template_methods.rb +125 -0
- data/lib/kiss.rb +725 -0
- metadata +108 -0
data/lib/kiss/mailer.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
class Kiss
|
2
|
+
# This class creates, renders, and sends email messages.
|
3
|
+
class Mailer
|
4
|
+
include Kiss::TemplateMethods
|
5
|
+
|
6
|
+
# Class Methods
|
7
|
+
|
8
|
+
def self.set_controller(controller)
|
9
|
+
@@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
# Instance Methods
|
13
|
+
|
14
|
+
# Creates new email message object.
|
15
|
+
def initialize(options = {})
|
16
|
+
@options = {
|
17
|
+
:engine => :sendmail
|
18
|
+
}.merge(options)
|
19
|
+
|
20
|
+
@data = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Invokes controller's file_cache.
|
24
|
+
def file_cache(*args,&block)
|
25
|
+
controller.file_cache(*args,&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def controller
|
29
|
+
@@controller
|
30
|
+
end
|
31
|
+
|
32
|
+
# Renders email template to string, unless message option is
|
33
|
+
# already set to a string value.
|
34
|
+
def prepare_email_message(options = {})
|
35
|
+
@options.merge!(options)
|
36
|
+
|
37
|
+
unless @options[:message].is_a?(String)
|
38
|
+
if template_name = @options[:template]
|
39
|
+
@template_dir = @@controller.email_template_dir
|
40
|
+
raise 'email_template_dir path not set' unless @template_dir
|
41
|
+
|
42
|
+
data = vars = @options[:data] || @options[:vars]
|
43
|
+
|
44
|
+
path = "#{@template_dir}/#{template_name}"
|
45
|
+
@options[:message] = erubis(path,binding)
|
46
|
+
else
|
47
|
+
raise 'email message not defined'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return @options[:message]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sets data to be passed to email template.
|
55
|
+
def set(key,value)
|
56
|
+
@data[key] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Attempts to send message using SMTP, unless :engine option is set to
|
60
|
+
# :sendmail.
|
61
|
+
def send(options)
|
62
|
+
@options.merge!(options)
|
63
|
+
if options[:engine] == :sendmail
|
64
|
+
return sendmail
|
65
|
+
else
|
66
|
+
return send_smtp
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Attempts to send message using /usr/sbin/sendmail.
|
71
|
+
def sendmail(options = {})
|
72
|
+
prepare_email_message(options)
|
73
|
+
|
74
|
+
IO.popen(@options[:sendmail_path] || "/usr/sbin/sendmail -t","w") do |pipe|
|
75
|
+
pipe.puts(@options[:message])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Attempts to send message using Net::SMTP.
|
80
|
+
def send_smtp(options = {})
|
81
|
+
prepare_email_message(options)
|
82
|
+
|
83
|
+
require 'net/smtp' unless defined?(Net::SMTP)
|
84
|
+
# begin
|
85
|
+
Net::SMTP.start('localhost') do |smtp|
|
86
|
+
smtp.sendmail(@options[:message], options[:from], options[:to])
|
87
|
+
end
|
88
|
+
# rescue
|
89
|
+
# end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/kiss/model.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
class Kiss
|
2
|
+
# This class adds functionality to Sequel::Model and automatically loads
|
3
|
+
# model class definitions from model_dir. It also uses Kiss#file_cache
|
4
|
+
# to cache database model classes, unless no model_dir is specified.
|
5
|
+
class Model < Sequel::Model
|
6
|
+
class << self
|
7
|
+
def name
|
8
|
+
@table.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
# Name symbol for default foreign key
|
12
|
+
def default_remote_key
|
13
|
+
:"#{name.singularize.demodulize.underscore}_id"
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_controller(controller)
|
17
|
+
@@controller = controller
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_dataset(source)
|
21
|
+
super(source)
|
22
|
+
end
|
23
|
+
|
24
|
+
def table=(table)
|
25
|
+
@table = table
|
26
|
+
end
|
27
|
+
|
28
|
+
def controller
|
29
|
+
@@controller
|
30
|
+
end
|
31
|
+
|
32
|
+
def dbm
|
33
|
+
@@controller.dbm
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: Fix has_many and many_to_many associations
|
37
|
+
def associate(type, name, opts = {}, &block)
|
38
|
+
opts = opts.clone
|
39
|
+
|
40
|
+
unless opts[:class] || opts[:class_name]
|
41
|
+
opts[:class_name] = name.to_s.pluralize
|
42
|
+
end
|
43
|
+
|
44
|
+
super(type, name, opts, &block)
|
45
|
+
|
46
|
+
association_reflections[name]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(meth)
|
51
|
+
raise NoMethodError, "undefined method `#{meth}' for database model `#{self.class.name}'"
|
52
|
+
end
|
53
|
+
|
54
|
+
def controller
|
55
|
+
@@controller
|
56
|
+
end
|
57
|
+
|
58
|
+
include Kiss::ControllerAccessors
|
59
|
+
end
|
60
|
+
|
61
|
+
class ModelCache
|
62
|
+
def initialize(controller,model_dir = nil)
|
63
|
+
@controller = controller
|
64
|
+
@model_dir = model_dir && File.directory?(model_dir) ? model_dir : nil
|
65
|
+
@cache = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](source)
|
69
|
+
(@model_dir && source.is_a?(Symbol)) ? begin
|
70
|
+
# use controller's file_cache
|
71
|
+
model_path = "#{@model_dir}/#{source}.rb"
|
72
|
+
@controller.file_cache(model_path) do |src|
|
73
|
+
klass = Class.new(Kiss::Model)
|
74
|
+
klass.set_dataset(Model.db[source])
|
75
|
+
klass.table = source
|
76
|
+
klass.class_eval(src,model_path) if src
|
77
|
+
klass
|
78
|
+
end
|
79
|
+
end : begin
|
80
|
+
# no model_dir, or source is not a symbol
|
81
|
+
# no mapping from source to filesystem path
|
82
|
+
# use ModelCache's own cache
|
83
|
+
@cache[source] ||= begin
|
84
|
+
klass = Class.new(Kiss::Model)
|
85
|
+
klass.set_dataset(Model.db[source])
|
86
|
+
klass.table = source if source.is_a?(Symbol)
|
87
|
+
klass
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def db
|
93
|
+
Sequel::Model.db
|
94
|
+
end
|
95
|
+
|
96
|
+
def literal(*args)
|
97
|
+
Sequel::Model.dataset.literal(*args)
|
98
|
+
end
|
99
|
+
alias_method :quote, :literal
|
100
|
+
|
101
|
+
def mdy_to_ymd(*args)
|
102
|
+
Kiss.mdy_to_ymd(*args)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
Sequel::Model::Associations::AssociationReflection.class_eval do
|
108
|
+
def associated_class
|
109
|
+
self[:class] ||= Kiss::Model.controller.dbm[self[:class_name].to_s.pluralize.to_sym]
|
110
|
+
end
|
111
|
+
def default_left_key
|
112
|
+
:"#{self[:model].name.singularize.underscore}_id"
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
def bench(label = nil)
|
2
|
+
Rack::Bench.close_bench_item(Kernel.caller[0])
|
3
|
+
Rack::Bench.on
|
4
|
+
|
5
|
+
if label
|
6
|
+
Rack::Bench.push_bench_item(
|
7
|
+
:label => label,
|
8
|
+
:start_time => Time.now,
|
9
|
+
:start_context => Kernel.caller[0]
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Rack
|
15
|
+
# Rack::Bench adds benchmarking capabilities to Kiss applications.
|
16
|
+
#
|
17
|
+
# bench(label) starts a new timer, which ends upon the next call to
|
18
|
+
# bench, or when execution returns to Rack::Bench.
|
19
|
+
#
|
20
|
+
# bench can be called without a label to end the previous timer
|
21
|
+
# without starting a new one.
|
22
|
+
#
|
23
|
+
# Total request duration is also displayed for any request in which
|
24
|
+
# the bench function is called.
|
25
|
+
class Bench
|
26
|
+
def self.on
|
27
|
+
@@bench = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.close_bench_item(end_context = nil)
|
31
|
+
if @@bench_items[-1] && !@@bench_items[-1][:end_time]
|
32
|
+
@@bench_items[-1][:end_time] = Time.now
|
33
|
+
@@bench_items[-1][:end_context] = end_context
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.push_bench_item(item)
|
38
|
+
@@bench_items.push(item)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(app)
|
42
|
+
@app = app
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
@@bench = false
|
47
|
+
@@bench_items = []
|
48
|
+
|
49
|
+
start_time = Time.now
|
50
|
+
code, headers, body = @app.call(env)
|
51
|
+
end_time = Time.now
|
52
|
+
|
53
|
+
if @@bench
|
54
|
+
Rack::Bench.close_bench_item
|
55
|
+
contents = <<-EOT
|
56
|
+
<style>
|
57
|
+
.kiss_bench {
|
58
|
+
text-align: left;
|
59
|
+
padding: 3px 7px;
|
60
|
+
border: 1px solid #ec4;
|
61
|
+
border-top: 1px solid #fff4bb;
|
62
|
+
border-bottom: 1px solid #d91;
|
63
|
+
background-color: #ffe590;
|
64
|
+
font-size: 12px;
|
65
|
+
color: #101;
|
66
|
+
}
|
67
|
+
.kiss_bench a {
|
68
|
+
color: #930;
|
69
|
+
text-decoration: none;
|
70
|
+
}
|
71
|
+
.kiss_bench a:hover {
|
72
|
+
color: #930;
|
73
|
+
text-decoration: underline;
|
74
|
+
}
|
75
|
+
</style>
|
76
|
+
EOT
|
77
|
+
|
78
|
+
contents += @@bench_items.map do |item|
|
79
|
+
start_link = context_link(item[:start_context])
|
80
|
+
end_link = context_link(item[:end_context])
|
81
|
+
|
82
|
+
<<-EOT
|
83
|
+
<div class="kiss_bench">
|
84
|
+
<tt><b>#{item[:label].gsub(/\</,'<')} duration: #{sprintf("%0.3f",item[:end_time].to_f - item[:start_time].to_f)} s</b></tt>
|
85
|
+
<small style="line-height: 105%; display: block; padding-bottom: 3px">kiss bench<br/>started at #{start_link}<br/>ended at #{end_link || 'return to kiss bench'}</small>
|
86
|
+
</div>
|
87
|
+
EOT
|
88
|
+
end.join
|
89
|
+
|
90
|
+
contents += <<-EOT
|
91
|
+
<div class="kiss_bench">
|
92
|
+
<tt><b>TOTAL request duration: #{sprintf("%0.3f",end_time.to_f - start_time.to_f)} s</b></tt>
|
93
|
+
<br><small>kiss bench request total</small>
|
94
|
+
</div>
|
95
|
+
EOT
|
96
|
+
else
|
97
|
+
contents = ''
|
98
|
+
end
|
99
|
+
|
100
|
+
body.each {|p| contents += p }
|
101
|
+
headers['Content-Length'] = contents.length.to_s
|
102
|
+
|
103
|
+
[ code, headers, contents ]
|
104
|
+
end
|
105
|
+
|
106
|
+
def absolute_path(filename)
|
107
|
+
filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
|
108
|
+
end
|
109
|
+
|
110
|
+
def context_link(context)
|
111
|
+
return nil unless context
|
112
|
+
|
113
|
+
filename, line, method = context.split(/:/)
|
114
|
+
textmate_url = "txmt://open?url=file://" + h(absolute_path(filename)) + '&line=' + line
|
115
|
+
%Q(<a href="#{textmate_url}">#{filename}:#{line}</a> #{method})
|
116
|
+
end
|
117
|
+
|
118
|
+
def textmate_href(frame)
|
119
|
+
"txmt://open?url=file://" + h(absolute_path(context)).sub(/:/,'&line=')
|
120
|
+
end
|
121
|
+
|
122
|
+
def h(obj) # :nodoc:
|
123
|
+
case obj
|
124
|
+
when String
|
125
|
+
Utils.escape_html(obj).gsub(/^(\s+)/) {' ' * $1.length}
|
126
|
+
else
|
127
|
+
Utils.escape_html(obj.inspect)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::EmailErrors sends error responses (code 5xx) to email addresses as
|
3
|
+
# specified in the Rack::Builder config.
|
4
|
+
class EmailErrors
|
5
|
+
def initialize(app,agent,app_name,from,*to)
|
6
|
+
@app = app
|
7
|
+
@agent = agent == :sendmail ? '/usr/sbin/sendmail -t' : agent
|
8
|
+
@app_name = app_name
|
9
|
+
@from = from
|
10
|
+
@to = to.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
code, headers, body = @app.call(env)
|
15
|
+
|
16
|
+
if code >= 500 && code < 600
|
17
|
+
begin # rescue any errors in message composition and sending
|
18
|
+
error_type = headers['X-Kiss-Error-Type'] || "#{code} Error"
|
19
|
+
error_message = headers['X-Kiss-Error-Message']
|
20
|
+
|
21
|
+
message = <<-EOT
|
22
|
+
Content-type: text/html
|
23
|
+
From: #{@from}
|
24
|
+
To: #{@to.join(', ')}
|
25
|
+
Subject: #{@app_name} - #{error_type}#{ error_message ? ": #{error_message}" : ''}
|
26
|
+
|
27
|
+
EOT
|
28
|
+
|
29
|
+
body.each do |part|
|
30
|
+
message += part
|
31
|
+
end
|
32
|
+
|
33
|
+
if @agent.is_a?(String)
|
34
|
+
IO.popen(@agent,"w") do |pipe|
|
35
|
+
pipe.puts(message)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
require 'net/smtp' unless defined?(Net::SMTP)
|
39
|
+
smtp = @agent.is_a?(Net::SMTP) ? @agent : Net::SMTP.new('localhost')
|
40
|
+
smtp.start do |smtp|
|
41
|
+
smtp.send_message(message, @from, *@to)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
|
47
|
+
body = <<-EOT
|
48
|
+
<html>
|
49
|
+
<head>
|
50
|
+
<title>Error</title>
|
51
|
+
</head>
|
52
|
+
<body>
|
53
|
+
<h1>Application Server Error</h1>
|
54
|
+
<p>Sorry, an error occurred. Our technical staff has been notified and will investigate this issue.</p>
|
55
|
+
</body>
|
56
|
+
</html>
|
57
|
+
EOT
|
58
|
+
headers['Content-Length'] = body.length.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
[ code, headers, body ]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::Facebook formats HTTP responses to remove certain status codes
|
3
|
+
# and HTML entities that are invalid as FBML responses.
|
4
|
+
class Facebook
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
code, headers, body = @app.call(env)
|
11
|
+
|
12
|
+
if code >= 500 && code < 600
|
13
|
+
code = 200
|
14
|
+
end
|
15
|
+
|
16
|
+
contents = ''
|
17
|
+
body.each {|p| contents += p }
|
18
|
+
|
19
|
+
contents.gsub!(/txmt:\/\//, 'http://textmate.local/')
|
20
|
+
contents.gsub!('<body>','<div class="body">')
|
21
|
+
contents.gsub!('</body>','</div>')
|
22
|
+
|
23
|
+
headers['Content-Length'] = contents.length.to_s
|
24
|
+
|
25
|
+
[ code, headers, contents ]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::FileNotFound rescues Kiss::TemplateFileNotFound exceptions
|
3
|
+
# (raised when action template files are not found) and returns an
|
4
|
+
# HTTP 404 error response.
|
5
|
+
class FileNotFound
|
6
|
+
def initialize(app,path = nil)
|
7
|
+
@app = app
|
8
|
+
@body = path ? (
|
9
|
+
::File.file?(path) ?
|
10
|
+
::File.read(path) :
|
11
|
+
template('could not find specified FileNotFound error document')
|
12
|
+
) : template
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
code, headers, body = @app.call(env)
|
17
|
+
rescue Kiss::TemplateFileNotFound => e
|
18
|
+
[ 404, {
|
19
|
+
"Content-Type" => "text/html",
|
20
|
+
"Content-Length" => @body.length.to_s
|
21
|
+
}, @body ]
|
22
|
+
end
|
23
|
+
|
24
|
+
def template(error = nil)
|
25
|
+
<<EOT
|
26
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
27
|
+
<html lang="en">
|
28
|
+
<head>
|
29
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
30
|
+
<meta name="robots" content="NONE,NOARCHIVE" />
|
31
|
+
<title>File Not Found</title>
|
32
|
+
</head>
|
33
|
+
<body>
|
34
|
+
<h1>404 File Not Found</h1>
|
35
|
+
|
36
|
+
#{error ? "<p>Additionally, #{error}.</p>" : ''}
|
37
|
+
</body>
|
38
|
+
</html>
|
39
|
+
EOT
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::ShowExceptions catches exceptions raised from the app,
|
3
|
+
# showing a useful backtrace with clickable stack frames and
|
4
|
+
# TextMate links to source files, as well as the last database
|
5
|
+
# query, GET/POST params, cookies, and Rack environment variables.
|
6
|
+
#
|
7
|
+
# Be careful using this on public-facing sites as it could reveal
|
8
|
+
# potentially sensitive information to malicious users.
|
9
|
+
|
10
|
+
class LogExceptions
|
11
|
+
def initialize(app,path)
|
12
|
+
@app = app
|
13
|
+
@@file = ::File.open(path,'w')
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
@app.call(env)
|
18
|
+
rescue StandardError, LoadError, SyntaxError => e
|
19
|
+
@@file.print Kiss::ExceptionReport.generate(env, e) + "\n--- End of exception report --- \n\n"
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
$debug_messages = []
|
2
|
+
def debug(object)
|
3
|
+
$debug_messages.push( [object.inspect, Kernel.caller[0]] )
|
4
|
+
object
|
5
|
+
end
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
# Rack::ShowDebug displays messages logged by the debug function.
|
9
|
+
#
|
10
|
+
# Be careful using this on public-facing sites as it could reveal
|
11
|
+
# potentially sensitive information to malicious users.
|
12
|
+
|
13
|
+
class ShowDebug
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
$debug_messages = []
|
20
|
+
code, headers, body = @app.call(env)
|
21
|
+
|
22
|
+
if $debug_messages.size > 0
|
23
|
+
contents = <<-EOT
|
24
|
+
<style>
|
25
|
+
.kiss_debug {
|
26
|
+
text-align: left;
|
27
|
+
padding: 3px 7px;
|
28
|
+
border: 1px solid #ebe;
|
29
|
+
border-top: 1px solid #fdf;
|
30
|
+
border-bottom: 1px solid #d6d;
|
31
|
+
background-color: #fbf;
|
32
|
+
font-size: 12px;
|
33
|
+
color: #101;
|
34
|
+
}
|
35
|
+
.kiss_debug a {
|
36
|
+
color: #707;
|
37
|
+
text-decoration: none;
|
38
|
+
}
|
39
|
+
.kiss_debug a:hover {
|
40
|
+
color: #707;
|
41
|
+
text-decoration: underline;
|
42
|
+
}
|
43
|
+
</style>
|
44
|
+
EOT
|
45
|
+
contents += $debug_messages.map do |object,context|
|
46
|
+
filename, line, method = context.split(/:/)
|
47
|
+
textmate_url = "txmt://open?url=file://" + h(absolute_path(filename)) + '&line=' + line
|
48
|
+
<<-EOT
|
49
|
+
<div class="kiss_debug">
|
50
|
+
<tt><b>#{object.gsub(/\</,'<')}</b></tt>
|
51
|
+
<br><small>kiss debug output at <a href="#{textmate_url}">#{filename}:#{line}</a> #{method}</small>
|
52
|
+
</div>
|
53
|
+
EOT
|
54
|
+
end.join
|
55
|
+
else
|
56
|
+
contents = ''
|
57
|
+
end
|
58
|
+
|
59
|
+
body.each {|p| contents += p }
|
60
|
+
headers['Content-Length'] = contents.length.to_s
|
61
|
+
|
62
|
+
[ code, headers, contents ]
|
63
|
+
end
|
64
|
+
|
65
|
+
def absolute_path(filename)
|
66
|
+
filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
|
67
|
+
end
|
68
|
+
|
69
|
+
def textmate_href(frame)
|
70
|
+
"txmt://open?url=file://" + h(absolute_path(context)).sub(/:/,'&line=')
|
71
|
+
end
|
72
|
+
|
73
|
+
def h(obj) # :nodoc:
|
74
|
+
case obj
|
75
|
+
when String
|
76
|
+
Utils.escape_html(obj).gsub(/^(\s+)/) {' ' * $1.length}
|
77
|
+
else
|
78
|
+
Utils.escape_html(obj.inspect)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::ShowExceptions catches exceptions raised from the app,
|
3
|
+
# showing a useful backtrace with clickable stack frames and
|
4
|
+
# TextMate links to source files, as well as the last database
|
5
|
+
# query, GET/POST params, cookies, and Rack environment variables.
|
6
|
+
#
|
7
|
+
# Be careful using this on public-facing sites as it could reveal
|
8
|
+
# potentially sensitive information to malicious users.
|
9
|
+
|
10
|
+
class ShowExceptions
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
@app.call(env)
|
17
|
+
rescue StandardError, LoadError, SyntaxError => e
|
18
|
+
body = Kiss::ExceptionReport.generate(env, e)
|
19
|
+
[500, {
|
20
|
+
"Content-Type" => "text/html",
|
21
|
+
"Content-Length" => body.length.to_s,
|
22
|
+
"X-Kiss-Error-Type" => e.class.name,
|
23
|
+
"X-Kiss-Error-Message" => e.message.sub(/\n.*/m,'')
|
24
|
+
}, body]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sequel
|
2
|
+
module MySQL
|
3
|
+
class Dataset < Sequel::Dataset
|
4
|
+
# Returns results from dataset query as array of arrays,
|
5
|
+
# instead of array of hashes.
|
6
|
+
def all_arrays(opts = nil, &block)
|
7
|
+
a = []
|
8
|
+
fetch_arrays(select_sql(opts)) {|r| a << r}
|
9
|
+
a.each(&block) if block
|
10
|
+
a
|
11
|
+
end
|
12
|
+
|
13
|
+
# Fixes bug in Sequel 1.5; shouldn't be needed for Sequel 2.x
|
14
|
+
# (need to double-check, however).
|
15
|
+
def fetch_arrays(sql)
|
16
|
+
@db.execute_select(sql) do |r|
|
17
|
+
r.each_array {|row| yield row}
|
18
|
+
end
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|