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.
@@ -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(/\</,'&lt;')} 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)) + '&amp;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(/:/,'&amp;line=')
120
+ end
121
+
122
+ def h(obj) # :nodoc:
123
+ case obj
124
+ when String
125
+ Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $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)) + '&amp;line=' + line
48
+ <<-EOT
49
+ <div class="kiss_debug">
50
+ <tt><b>#{object.gsub(/\</,'&lt;')}</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(/:/,'&amp;line=')
71
+ end
72
+
73
+ def h(obj) # :nodoc:
74
+ case obj
75
+ when String
76
+ Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $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