kiss 1.0.4 → 1.1

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/Rakefile CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'rubygems'
2
- Gem::manage_gems
2
+ require 'rubygems/builder'
3
3
  require 'rake/gempackagetask'
4
4
 
5
5
  spec = Gem::Specification.new do |s|
@@ -11,16 +11,19 @@ spec = Gem::Specification.new do |s|
11
11
  s.homepage = 'http://www.rubykiss.org'
12
12
  s.rubyforge_project = 'kiss'
13
13
  s.summary = 'An MVC web application framework using Rack, Sequel, and Erubis.'
14
- s.files = %w( Rakefile VERSION LICENSE ) + Dir['{lib,tests}/**/*']
14
+ s.files = %w( Rakefile VERSION LICENSE ) + Dir['{bin,data,lib,tests}/**/*']
15
15
  s.require_path = 'lib'
16
16
  s.test_files = Dir.glob('tests/*.rb')
17
17
  s.has_rdoc = true
18
+ s.executables = %w( kiss )
18
19
 
19
- s.rdoc_options << '--main' << 'Kiss' <<
20
- '--title' << 'Kiss: Ruby MVC Web Framework -- RDoc Reference Documentation'
20
+ s.rdoc_options.push(
21
+ '--main', 'Kiss',
22
+ '--title', 'Kiss: Ruby MVC Web Framework -- RDoc Reference Documentation'
23
+ )
21
24
 
22
- s.add_dependency('rack')
23
- s.add_dependency('sequel','>=2.2.0')
25
+ s.add_dependency('rack','=0.4.0')
26
+ s.add_dependency('sequel','>=2.5.0')
24
27
  s.add_dependency('erubis')
25
28
  end
26
29
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.4
1
+ 1.1
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ def create
5
+ project_name = ARGV[0]
6
+ display_usage('create') unless project_name =~ /\S/
7
+
8
+ path = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))),'data','scaffold.tgz')
9
+
10
+ `mkdir #{project_name}`
11
+
12
+ print "Expanding '#{path}' to '#{project_name}'..."
13
+
14
+ `cd '#{ARGV[0]}'; tar xvfz '#{path}'`
15
+
16
+ puts 'done.'
17
+ end
18
+
19
+ def run
20
+ project_name = ARGV[0]
21
+ project_name = '.' unless project_name =~ /\S/
22
+
23
+ require 'rubygems'
24
+ require 'kiss'
25
+
26
+ Kiss.run( :project_dir => project_name )
27
+ end
28
+
29
+ def display_usage(command = nil)
30
+ case command
31
+ when 'create'
32
+ puts <<-EOT
33
+ usage: kiss create <app_directory>
34
+ Creates a skeleton for a new Kiss application.
35
+ EOT
36
+ when 'run'
37
+ puts <<-EOT
38
+ usage: kiss run [app_directory]
39
+ Runs a Kiss application located within app_directory.
40
+
41
+ The app_directory path may be omitted when running this command from
42
+ within the top-level directory of a Kiss application.
43
+ EOT
44
+ else
45
+ puts <<-EOT
46
+ usage: kiss <command> [arguments]
47
+ Utility to support and facilitate the Kiss web framework.
48
+
49
+ Commands:
50
+ create - Create a new Kiss application project.
51
+ run - Run a Kiss application.
52
+ help - Display usage info.
53
+
54
+ For help on any command above, type:
55
+ kiss help <command>
56
+ EOT
57
+ end
58
+ puts
59
+ exit
60
+ end
61
+
62
+
63
+ # main
64
+
65
+ case (command = ARGV.shift)
66
+ when 'create'
67
+ create
68
+ when 'run'
69
+ run
70
+ when 'help'
71
+ display_usage(ARGV[0])
72
+ else
73
+ display_usage
74
+ end
75
+ exit
Binary file
@@ -22,10 +22,12 @@ autoload :Sequel, 'sequel'
22
22
 
23
23
  module Rack
24
24
  autoload :Bench, 'kiss/rack/bench'
25
+ autoload :ErrorsOK, 'kiss/rack/errors_ok'
25
26
  autoload :EmailErrors, 'kiss/rack/email_errors'
26
27
  autoload :Facebook, 'kiss/rack/facebook'
27
28
  autoload :FileNotFound, 'kiss/rack/file_not_found'
28
29
  autoload :LogExceptions, 'kiss/rack/log_exceptions'
30
+ autoload :Recorder, 'kiss/rack/recorder'
29
31
  autoload :ShowDebug, 'kiss/rack/show_debug'
30
32
  autoload :ShowExceptions, 'kiss/rack/show_exceptions'
31
33
  end
@@ -64,6 +66,7 @@ class Kiss
64
66
  autoload :ExceptionReport, 'kiss/exception_report'
65
67
  autoload :Mailer, 'kiss/mailer'
66
68
  autoload :Iterator, 'kiss/iterator'
69
+ autoload :Login, 'kiss/login'
67
70
  autoload :Form, 'kiss/form'
68
71
  autoload :Format, 'kiss/format'
69
72
  autoload :SequelSession, 'kiss/sequel_session'
@@ -88,10 +91,7 @@ class Kiss
88
91
  :evolution_dir, :asset_dir, :public_dir, :environment, :options, :layout, :rack_file
89
92
 
90
93
  # attributes below are request-specific
91
- attr_reader :params, :args, :action, :action_subdir, :action_path, :extension, :host, :request,
92
- :exception_cache
93
-
94
- attr_accessor :last_sql
94
+ attr_reader :protocol, :host, :request, :exception_cache, :env
95
95
 
96
96
  @@default_action = 'index'
97
97
  @@default_cookie_name = 'Kiss'
@@ -133,12 +133,12 @@ class Kiss
133
133
  unless builder_option.is_a?(Class)
134
134
  builder_option = Rack.const_get(builder_option.to_s)
135
135
  end
136
-
136
+
137
137
  use(builder_option,*builder_args)
138
138
  end
139
-
139
+
140
140
  run app
141
- end
141
+ end.to_app
142
142
 
143
143
  handler = @@options[:rack_handler] || Rack::Handler::WEBrick
144
144
  if !handler.is_a?(Class)
@@ -148,7 +148,7 @@ class Kiss
148
148
  rescue StandardError, LoadError, SyntaxError => e
149
149
  if @@options[:rack_handler] == :CGI
150
150
  print "Content-type: text/html\n\n"
151
- print Kiss::ExceptionReport.generate(e)
151
+ print $debug_messages.to_s + Kiss::ExceptionReport.generate(e)
152
152
  else
153
153
  print "Content-type: text/plain\n\n"
154
154
  puts "exception:\n" + e.message
@@ -195,7 +195,7 @@ class Kiss
195
195
  @@lib_dirs = ['lib']
196
196
  @@gem_dirs = ['gems']
197
197
  @@require = []
198
- @@authenticate_exclude = ['/logout']
198
+ @@authenticate_exclude = ['/login','/logout']
199
199
 
200
200
  # common (shared) config
201
201
  if (File.file?(config_file = @@config_dir+'/common.yml'))
@@ -220,7 +220,7 @@ class Kiss
220
220
 
221
221
  @@email_template_dir = @@options[:email_template_dir] || 'email_templates'
222
222
  @@upload_dir = @@options[:upload_dir] || 'uploads'
223
-
223
+
224
224
  @@cookie_name = @@options[:cookie_name] || @@default_cookie_name
225
225
  @@default_action = @@options[:default_action] || @@default_action
226
226
  @@action_root_class = @@options[:action_class] || Class.new(Kiss::Action)
@@ -231,14 +231,20 @@ class Kiss
231
231
  # default layout
232
232
  @@layout = @@options[:layout]
233
233
 
234
- # public_uri: uri of requests to serve from public_dir
235
- @@asset_uri = @@public_uri = @@options[:asset_uri] || @@options[:public_uri]
236
- @@rack_file = Rack::File.new(@@asset_dir) if @@asset_uri
237
-
238
234
  # app_url: URL of the app actions root
239
- @@app_host = @@options[:app_host] ? ('http://' + @@options[:app_host]) : ''
235
+ @@protocol = ENV['HTTPS'] == 'on' ? 'https' : 'http'
236
+ @@app_host = @@options[:app_host] ? (@@protocol + '://' + @@options[:app_host]) : ''
240
237
  @@app_uri = @@options[:app_uri] || ''
241
238
  @@app_url = @@app_host + @@app_uri
239
+
240
+ # asset host: hostname of static assets
241
+ # (remove http:// prefix if present)
242
+ @@asset_host = @@options[:asset_host]
243
+ @@asset_host.sub!(/^http:\/\//,'') if @@asset_host
244
+
245
+ # public_uri: uri of requests to serve from public_dir
246
+ @@asset_uri = @@options[:asset_uri] || @@options[:public_uri] || ''
247
+ @@rack_file = Rack::File.new(@@asset_dir) if @@asset_uri
242
248
 
243
249
  # include lib dirs
244
250
  $LOAD_PATH.unshift(*( @@lib_dirs.flatten.select {|dir| File.directory?(dir) } ))
@@ -250,16 +256,18 @@ class Kiss
250
256
  @@require.flatten.each {|lib| require lib }
251
257
 
252
258
  # session class
253
- if (@@options[:session_class])
254
- @@session_class = (@@options[:session_class].class == Class) ? @@options[:session_class] :
255
- @@options[:session_class].to_const
256
- end
259
+ @@session_class = @@options[:session_class] ?
260
+ (@@session_class = (@@options[:session_class].class == Class) ?
261
+ @@options[:session_class] : @@options[:session_class].to_const
262
+ ) : nil
257
263
 
258
264
  # load extensions to action class
259
265
  action_extension_path = @@action_dir + '/_action.rb'
260
266
  if File.file?(action_extension_path)
261
267
  @@action_root_class.class_eval(File.read(action_extension_path),action_extension_path) rescue nil
262
268
  end
269
+
270
+ @@action_classes = {}
263
271
 
264
272
  # database
265
273
  @@database_config = @@options[:database]
@@ -267,14 +275,7 @@ class Kiss
267
275
 
268
276
  self
269
277
  end
270
-
271
- # Returns URL/URI of app's static assets (asset_host or public_uri).
272
- def assets(suffix = nil)
273
- @@pub ||= @@options[:asset_host]
274
- suffix ? @@pub + '/' + suffix : @@pub
275
- end
276
- alias_method :pub, :assets
277
-
278
+
278
279
  # Returns true if specified path is a directory.
279
280
  # Cache result if file_cache_no_reload option is set; otherwise, always check filesystem.
280
281
  def directory_exists?(dir)
@@ -313,14 +314,23 @@ class Kiss
313
314
  filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
314
315
  end
315
316
 
317
+ # prepend addition just inside the specified tag of document
318
+ def html_prepend(addition,document,tag)
319
+ document = document.body unless document.is_a?(String)
320
+ document.sub(/(\<#{tag}[^\>]*\>)/i, '\1'+addition)
321
+ end
322
+
316
323
  # Returns string representation of object with HTML entities escaped.
317
- def h(obj)
318
- case obj
319
- when String
320
- Rack::Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
321
- else
322
- Rack::Utils.escape_html(obj.inspect)
323
- end
324
+ def html_escape(obj)
325
+ Rack::Utils.escape_html(
326
+ obj.is_a?(String) ? obj : obj.inspect
327
+ ).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
328
+ end
329
+
330
+ # Escapes string for use in URLs.
331
+ def url_escape(string)
332
+ # encode space to '+'; don't encode letters, numbers, periods
333
+ string.gsub(/([^A-Za-z0-9\.])/) { sprintf("%%%02X", $&.unpack("C")[0]) }
324
334
  end
325
335
 
326
336
  # Returns MIME type corresponding to passed-in extension.
@@ -391,34 +401,55 @@ class Kiss
391
401
 
392
402
  # Given a file path, caches or returns the file's contents or the return value of
393
403
  # the passed block applied to the file's contents.
394
- # If file is not found, raises exception of type fnf_exception_class.
395
- def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound)
396
- if (@@options[:file_cache_no_reload] && @@file_cache.has_key?(path))
397
- return @@file_cache[path]
398
- end
404
+ # If file is not found, the file's contents are nil.
405
+ def file_cache(path, report_change = false)
406
+ raise 'nil file path cannot be cached' unless path
399
407
 
400
- if !File.file?(path)
401
- raise fnf_exception_class, "#{fnf_file_type} file missing: '#{path}'" if fnf_file_type
402
-
403
- @@file_cache[path] = nil
404
- contents = nil
408
+ if @@file_cache.has_key?(path) && @@options[:file_cache_no_reload]
409
+ # already loaded this path, and no_reload option is on; don't re-cache
410
+ change = false
411
+ elsif !@@file_cache.has_key?(path)
412
+ # haven't loaded this path yet
413
+ change = true
414
+ if !File.file?(path)
415
+ # nil path, of file doesn't exist
416
+ @@file_cache_time[path] = nil
417
+ contents = nil
418
+ else
419
+ # file exists; mark cache time and read file
420
+ @@file_cache_time[path] = Time.now
421
+ contents = File.read(path)
422
+ end
405
423
  else
406
- # expire cache if file (or symlink) modified
407
- # TODO: what about symlinks to symlinks?
408
- if !@@file_cache_time[path] ||
409
- @@file_cache_time[path] < File.mtime(path) ||
410
- ( File.symlink?(path) && (@@file_cache_time[path] < File.lstat(path).mtime) )
411
-
412
- @@file_cache[path] = nil
413
- @@file_cache_time[path] = Time.now
414
- contents = File.read(path)
424
+ # we've read this path before; may need to re-cache
425
+ if !File.file?(path)
426
+ if @@file_cache_time[path]
427
+ # file cached as existing but has been removed; update cache to show no file
428
+ change = true
429
+ @@file_cache_time[path] = nil
430
+ contents = nil
431
+ else
432
+ change = false
433
+ end
434
+ elsif !@@file_cache_time[path] ||
435
+ @@file_cache_time[path] < File.mtime(path) ||
436
+ ( File.symlink?(path) && (@@file_cache_time[path] < File.lstat(path).mtime) )
437
+ # cache shows file missing, or file has been modified since cached
438
+ change = true
439
+ @@file_cache_time[path] = Time.now
440
+ contents = File.read(path)
441
+ else
442
+ # cached file is still current; don't re-cache
443
+ change = false
415
444
  end
416
445
  end
417
446
 
418
- @@file_cache[path] ||= begin
419
- (block_given?) ? yield(contents) : contents
447
+ if change
448
+ @@file_cache[path] ||= block_given? ? yield(contents) : contents
420
449
  end
421
- end
450
+
451
+ report_change ? [@@file_cache[path], change] : @@file_cache[path]
452
+ end
422
453
  end # end class methods
423
454
 
424
455
  ### Instance Methods
@@ -438,49 +469,61 @@ class Kiss
438
469
  @files_cached_this_request = {}
439
470
  end
440
471
 
441
- # Caches the specified file and return its contents. See Kiss.file_cache above.
442
- def file_cache(path, fnf_file_type = nil, fnf_exception_class = Kiss::FileNotFound, &block)
443
- return @@file_cache[path] if @files_cached_this_request[path]
444
- @files_cached_this_request[path] = true
472
+ def invoke_action(path,params,render_options = {})
473
+ action, action_handler = get_action_handler(path,params)
474
+ catch :kiss_action_done do
475
+ action_handler.expand_login
445
476
 
446
- self.class.file_cache(path, fnf_file_type, fnf_exception_class, &block)
477
+ if @@options[:authenticate_all]
478
+ if (!@@authenticate_exclude.is_a?(Array) ||
479
+ @@authenticate_exclude.select {|a| a == action}.size == 0)
480
+ action_handler.authenticate
481
+ end
482
+ end
483
+
484
+ action_handler.call
485
+ action_handler.render(render_options)
486
+ end
487
+ action_handler
447
488
  end
448
489
 
449
490
  # Processes and responds to a request received via Rack. Should only be called once
450
491
  # for each controller instance (i.e. each controller instance should handle at most
451
492
  # one request, then be discarded). Returns array of response code, headers, and body.
452
493
  def call(env)
453
- if @@rack_file && (
454
- (env["PATH_INFO"] == '/favicon.ico') ||
455
- (env["PATH_INFO"].sub!(Regexp.new("\\A#{@@asset_uri}"),''))
456
- )
457
- return @@rack_file.call(env)
458
- end
494
+ @env = env
495
+
496
+ # if @@rack_file && (
497
+ # (env["PATH_INFO"] == '/favicon.ico') ||
498
+ # (env["PATH_INFO"].sub!(Regexp.new("\\A#{@@asset_uri}"),''))
499
+ # )
500
+ # return @@rack_file.call(env)
501
+ # end
459
502
 
460
503
  # catch and report exceptions in this block
461
504
 
462
505
  code, headers, body = begin
463
- get_request(env)
506
+ @request = Rack::Request.new(env)
464
507
  @response = Rack::Response.new
508
+
509
+ @protocol, @app_host = (@request.server rescue '').split(/\:\/\//, 2)
510
+ @app_host = @@options[:app_host] if @@options[:app_host]
511
+ @app_uri = @@options[:app_uri] || @request.script_name || ''
512
+
513
+ @host ||= @request.host rescue ''
514
+
515
+ path = @request.path_info || '/'
516
+ params = @request.params
465
517
 
466
- catch :kiss_action_done do
467
- parse_action_path(@path)
468
- env['kiss.parsed_action'] = @action
469
- env['kiss.parsed_args'] = @args.inspect
470
-
471
- redirect_url(app_url + '/') if @action == '/login'
518
+ catch :kiss_request_done do
519
+ action_handler = invoke_action(path,params)
520
+ extension = action_handler.extension
472
521
 
473
- if login_session_valid?
474
- load_from_login_session
475
- elsif @@options[:authenticate_all]
476
- if (!@@authenticate_exclude.is_a?(Array) ||
477
- @@authenticate_exclude.select {|action| action == @action}.size == 0)
478
- authenticate
479
- end
522
+ if content_type = options[:content_type] || (extension ? Kiss.mime_type(extension) : nil)
523
+ @response['Content-Type'] = "#{content_type}; #{options[:document_encoding] || 'utf-8'}"
480
524
  end
481
525
 
482
- env['kiss.processed_action'] = @action
483
- process.render
526
+ send_response(action_handler.output, action_handler.output_options)
484
527
  end
485
528
  finalize_session if @session
486
529
 
@@ -511,56 +554,159 @@ class Kiss
511
554
  headers['Content-Length'] = body.length.to_s
512
555
  end
513
556
 
514
- @@database_pool.push(@db) if @db
557
+ if @db
558
+ @db.kiss_controller = nil
559
+ @@database_pool.push(@db)
560
+ end
515
561
 
516
562
  [code,headers,body]
517
563
  end
518
564
 
519
- # Adds debug message to inspect object. Debug messages will be shown at top of
520
- # application response body.
521
- def debug(object, context = Kernel.caller[0])
522
- @debug_messages.push( [object.inspect, context] )
523
- object
524
- end
525
565
 
526
- # Starts a new benchmark timer, with optional label. Benchmark results will be shown
527
- # at top of application response body.
528
- def bench(label = nil, context = Kernel.caller[0])
529
- stop_benchmark(context)
530
- @benchmarks.push(
531
- :label => label,
532
- :start_time => Time.now,
533
- :start_context => context
534
- )
535
- end
566
+ ##### ACTION METHODS #####
536
567
 
537
- # Stops last benchmark timer, if still running.
538
- def stop_benchmark(end_context = nil)
539
- if @benchmarks[-1] && !@benchmarks[-1][:end_time]
540
- @benchmarks[-1][:end_time] = Time.now
541
- @benchmarks[-1][:end_context] = end_context
568
+ # Parses request URI to determine action path and arguments, then
569
+ # instantiates action class to create action handler.
570
+ def get_action_handler(path,params)
571
+ action_subdir = '/'
572
+ action_uri = ''
573
+ action = nil
574
+ args = []
575
+
576
+ redirect_url(app_url + '/') if path == ''
577
+
578
+ path += @@default_action if path =~ /\/\Z/
579
+
580
+ parts = path.sub(/^\/*/,'').split('/')
581
+
582
+ action_class = Kiss::Action.get_subclass('/', @@action_dir + '/_action.rb', self) ||
583
+ Kiss::Action
584
+
585
+ while part = parts.shift
586
+ action_uri += '/' + part
587
+
588
+ if part =~ /\A(\d+)\Z/ || part =~ /\A\=([\w\.\-]+)\Z/
589
+ args << $1
590
+ next
591
+ end
592
+
593
+ raise 'bad action path' if part !~ /\A[a-z][\w\-\.]*\Z/i
594
+
595
+ test_path = @@action_dir + action_subdir + part
596
+ if Kiss.directory_exists?(test_path)
597
+ action_subdir += part + '/'
598
+ action_class = action_class.get_subclass(
599
+ part + '/',
600
+ @@action_dir + action_subdir + '_action.rb',
601
+ self
602
+ )
603
+ next
604
+ end
605
+
606
+ if part =~ /\A(.+)\.(\w+)\Z/
607
+ extension = $2
608
+ part = $1
609
+ else
610
+ extension = 'rhtml'
611
+ end
612
+
613
+ action = action_subdir + part
614
+
615
+ action_class = action_class.get_subclass(part, @@action_dir + action + '.rb', self)
616
+ break
542
617
  end
543
- end
544
-
545
- # Sets up request-specified variables based in request information received from Rack.
546
- def get_request(env)
547
- @request = Rack::Request.new(env)
548
618
 
549
- @app_host = @@options[:app_host] ? ('http://' + @@options[:app_host]) : @request.server rescue ''
550
- @app_uri = @@options[:app_uri] || @request.script_name || ''
551
- @app_url = @app_host + @app_uri
619
+ # if no action, must have traversed all parts to a directory
620
+ # add a trailing slash and try again
621
+ redirect_url(app_url + action_subdir) unless action
552
622
 
553
- @path = @request.path_info || '/'
554
- @params = @request.params
623
+ # keep rest of path_info in args
624
+ args.push(*parts)
555
625
 
556
- @host ||= @request.host rescue ''
557
- @protocol = env['HTTPS'] == 'on' ? 'https' : 'http'
626
+ # return action path and action handler (instance of action class)
627
+ [action, action_class.new(self,action,action_uri,action_subdir,extension,args,params)]
558
628
  end
559
629
 
560
- # Returns URL/URI of app root (corresponding to top level of action_dir).
561
- def app_url(suffix = nil)
562
- suffix ? @app_url + suffix : @app_url
630
+
631
+ ##### FILE METHODS #####
632
+
633
+ # If file has already been cached in handling the current request, retrieve from cache
634
+ # and do not check filesystem for updates. Else cache file via Kiss.file_cache.
635
+ def file_cache(path, *args, &block)
636
+ return @@file_cache[path] if @files_cached_this_request[path]
637
+ @files_cached_this_request[path] = true
638
+
639
+ self.class.file_cache(path, *args, &block)
640
+ end
641
+
642
+
643
+ ##### DATABASE METHODS #####
644
+
645
+ # Acquires and returns a database connection object from the connection pool,
646
+ # opening a new connection if the pool is empty.
647
+ #
648
+ # Tip: `db' is a shorthand alias for `database'.
649
+ def database
650
+ @db ||= @@database_pool.shift || begin
651
+ raise 'database config missing' unless @@database_config
652
+
653
+ # open database connection (if not already open)
654
+ @db = @@database_config.is_a?(String) ? (Sequel.open @@database_config) :
655
+ @@database_config.is_a?(Hash) ? (Sequel.open @@database_config) : @@database_config
656
+
657
+ @@db_extras_loaded ||= begin
658
+ @db.class.class_eval do
659
+ attr_accessor :kiss_controller, :kiss_model_cache
660
+
661
+ @last_query = nil
662
+ def last_query #:nodoc:
663
+ @last_query
664
+ end
665
+
666
+ alias_method :execute_old, :execute
667
+ def execute(sql, *args, &block) #:nodoc:
668
+ @last_query = sql
669
+ execute_old(sql, *args, &block)
670
+ end
671
+ end
672
+
673
+ require 'kiss/model'
674
+ Kiss::ModelCache.model_dir = @@model_dir
675
+
676
+ if @db.class.name == 'Sequel::MySQL::Database'
677
+ # add fetch_arrays, all_arrays methods
678
+ require 'kiss/sequel_mysql'
679
+ # turn off convert_tinyint_to_bool, unless options say otherwise
680
+ Sequel.convert_tinyint_to_bool = false unless @@options[:convert_tinyint_to_bool]
681
+ end
682
+
683
+ true
684
+ end
685
+
686
+ # TODO: rewrite evolution file exists check for speed
687
+ check_evolution_number
688
+
689
+ # create model cache for this database connection
690
+ @db.kiss_model_cache = Kiss::ModelCache.new(@db)
691
+ @db.kiss_controller = self
692
+
693
+ @db
694
+ end
563
695
  end
696
+ alias_method :db, :database
697
+
698
+ # Kiss Model cache, used to invoke and store Kiss database models.
699
+ #
700
+ # Example:
701
+ # models[:users] == database model for `users' table
702
+ #
703
+ # Tip: `dbm' (stands for `database models') is a shorthand alias for `models'.
704
+ def models
705
+ # make sure we have a database connection
706
+ # create new model cache unless exists already
707
+ db.kiss_model_cache
708
+ end
709
+ alias_method :dbm, :models
564
710
 
565
711
  # Returns Sequel dataset to evolution_number table, which specifies app's current evolution number.
566
712
  # Creates evolution_number table if it does not exist.
@@ -595,13 +741,10 @@ class Kiss
595
741
  end
596
742
  end
597
743
 
598
- # Saves session to session store, if session data has changed since load.
599
- def finalize_session
600
- @session.save if @session_fingerprint != Marshal.dump(@session.data).hash
601
- end
602
744
 
603
- ##### LOGIN SESSION #####
745
+ ##### SESSION METHODS #####
604
746
 
747
+ # Retrieves or generates session data object, based on session ID from cookie value.
605
748
  def session
606
749
  @session ||= begin
607
750
  @@session_class ? begin
@@ -629,177 +772,35 @@ class Kiss
629
772
  end
630
773
  end
631
774
 
632
- def login
633
- @login ||= begin
634
- login = {}
635
-
636
- login.merge!(session[:login]) if (session[:login] && session[:login][:expires_at] &&
637
- session[:login][:expires_at] > Time.now)
638
-
639
- login
640
- end
641
- end
642
-
643
- # Deletes login data from session.
644
- def logout
645
- session.delete(:login)
646
- @login = nil
647
- end
648
- alias_method :reset_login_session, :logout
649
- alias_method :reset_login_data, :logout
650
-
651
- # Merges data hash (key-value pairs) into request login hash.
652
- def set_login_data(data)
653
- login.merge!(data)
654
- login
655
- end
656
-
657
- # Merges data hash (key-value pairs) into session login hash.
658
- def set_login_session(data)
659
- set_login_data(data)
660
- session[:login] = (session[:login] || {}).merge(data)
661
- end
662
-
663
- # Sets expire time of session login hash, after which time it will be reset (emptied).
664
- # If given a FixNum, sets expire time to now plus number of seconds.
665
- def set_login_expires(time)
666
- time = Time.now + time if time.is_a?(Fixnum)
667
- set_login_session(:expires_at => time)
668
- end
669
-
670
- # Returns true if login hash is defined and not expired.
671
- def login_session_valid?
672
- login && !login_session_expired?
673
- end
674
-
675
- # Returns true if login hash is expired.
676
- def login_session_expired?
677
- login && (!login[:expires_at] || login[:expires_at] < Time.now)
678
- end
679
-
680
- # Calls login action's load_from_session method to populate request login hash.
681
- def load_from_login_session
682
- klass = action_class('/login')
683
- raise 'load_from_login_session called, but no login action found' unless klass
684
-
685
- action_handler = klass.new(self)
686
- action_handler.load_from_session
687
- end
688
-
689
- # Returns path to login action.
690
- def login_path
691
- @@action_dir + '/login.rb'
692
- end
693
-
694
- # If valid login session exists, loads login action to populate request login hash data.
695
- # Otherwise, loads and calls login action to authenticate user.
696
- def authenticate
697
- if login_session_valid?
698
- load_from_login_session
699
- else
700
- klass = action_class('/login')
701
- raise 'authenticate called, but no login action found' unless klass
702
-
703
- old_template = @template
704
- @template = '/login.rhtml'
705
-
706
- process(klass,login_path)
707
-
708
- @template = old_template
709
-
710
- unless login_session_valid?
711
- #raise 'login action completed without setting valid login session'
712
- end
713
- end
775
+ # Saves session to session store, if session data has changed since load.
776
+ def finalize_session
777
+ @session.save if @session_fingerprint != Marshal.dump(@session.data).hash
714
778
  end
715
779
 
716
- ##### ACTION METHODS #####
717
-
718
- # Creates and caches anonymous class with which to invoke specified (or current) action.
719
- def action_class(action = @action)
720
- action_path = @@action_dir + action.to_s + '.rb'
721
- return nil unless action_path.is_a?(String) && action_path.length > 0
722
-
723
- file_cache(action_path) do |src|
724
- # create new action class, subclass of shared action parent class
725
- klass = Class.new(@@action_root_class)
726
- klass.class_eval(src,action_path) if src
727
- klass
728
- end
780
+ # Returns a Kiss::Login object containing data from session.login.
781
+ def login
782
+ @login ||= Kiss::Login.new(session)
729
783
  end
730
784
 
731
- # Parses request URI to determine action path and arguments.
732
- def parse_action_path(path)
733
- @action_subdir = ''
734
- @action = nil
735
-
736
- redirect_url(app_url + '/') if path == ''
737
-
738
- path += @@default_action if path =~ /\/\Z/
739
-
740
- parts = path.sub(/^\/*/,'').split('/')
741
-
742
- while part = parts.shift
743
- raise 'bad action' if part !~ /\A[a-z0-9][\w\-\.]*\Z/i
744
-
745
- test_path = @@action_dir + @action_subdir + '/' + part
746
- if Kiss.directory_exists?(test_path)
747
- @action_subdir += '/' + part
748
- next
749
- end
750
-
751
- if part =~ /\A(.+)\.(\w+)\Z/
752
- @extension = $2
753
- part = $1
754
- else
755
- @extension = 'rhtml'
756
- end
757
-
758
- @action = @action_subdir + '/' + part
759
- break
760
- end
761
-
762
- # if no action, must have traversed all parts to a directory
763
- # add a trailing slash and try again
764
- redirect_url(app_url + '/' + path + '/') unless @action
765
-
766
- @action_path = @@action_dir + '/' + @action + '.rb'
767
-
768
- # keep rest of path_info in args
769
- @args = parts
770
- end
771
785
 
772
- # Processes specified (or current) action, by instantiating its anonymous
773
- # action class and invoking `call' method on the instance.
774
- def process(klass = action_class, action_path = @action_path)
775
- action_handler = klass.new(self)
776
- action_handler.call
777
-
778
- # return handler to follow with render
779
- action_handler
780
- end
786
+ ##### OUTPUT METHODS #####
781
787
 
782
788
  # Outputs a Kiss::StaticFile object as response to Rack.
783
789
  # Used to return static files efficiently.
784
790
  def send_file(path, mime_type = nil)
785
791
  @response = Kiss::StaticFile.new(path,mime_type)
786
792
 
787
- throw :kiss_action_done
793
+ throw :kiss_request_done
788
794
  end
789
795
 
790
796
  # Prepares Rack::Response object to return application response to Rack.
791
797
  # Raises Kiss::ActionDone exception to bypass caller stack and return directly
792
798
  # to Kiss#call.
793
799
  def send_response(output = '',options = {})
794
- content_type = options[:content_type] ||
795
- (extension ? Kiss.mime_type(extension) : nil)
796
- document_encoding ||= 'utf-8'
797
-
798
- @response['Content-Type'] = "#{content_type}; #{document_encoding}" if content_type
799
800
  @response['Content-Length'] = output.length.to_s
800
801
  @response.body = output
801
802
 
802
- throw :kiss_action_done
803
+ throw :kiss_request_done
803
804
 
804
805
  # back to Kiss#call, which finalizes session and returns @response
805
806
  # (throws exception if no @response set)
@@ -809,81 +810,71 @@ class Kiss
809
810
  def redirect_url(url)
810
811
  @response.status = 302
811
812
  @response['Location'] = url
812
- send_response
813
+ @response.body = ''
814
+
815
+ throw :kiss_request_done
813
816
  end
814
817
 
815
- # Returns new Kiss::Mailer object using specified options.
816
- def new_email(options = {})
817
- mailer = Kiss::Mailer.new(options)
818
- mailer.controller = self
819
- mailer
820
- end
821
818
 
822
- def db
823
- @db ||= @@database_pool.shift || begin
824
- raise 'database config missing' unless @@database_config
825
-
826
- # open database connection (if not already open)
827
- @db = @@database_config.is_a?(String) ? (Sequel.open @@database_config) :
828
- @@database_config.is_a?(Hash) ? (Sequel.open @@database_config) : @@database_config
829
-
830
- @@db_extras_loaded ||= begin
831
- @db.class.class_eval do
832
- @last_query = nil
833
- def last_query
834
- @last_query
835
- end
819
+ ##### DEBUG/BENCH OUTPUT #####
836
820
 
837
- alias_method :execute_old, :execute
838
- def execute(sql, *args, &block)
839
- @last_query = sql
840
- execute_old(sql, *args, &block)
841
- end
842
- end
843
-
844
- require 'kiss/model'
845
- Kiss::ModelCache.model_dir = @@model_dir
846
-
847
- if @db.class.name == 'Sequel::MySQL::Database'
848
- # add fetch_arrays, all_arrays methods
849
- require 'kiss/sequel_mysql'
850
- # turn off convert_tinyint_to_bool, unless options say otherwise
851
- Sequel.convert_tinyint_to_bool = false unless @@options[:convert_tinyint_to_bool]
852
- end
853
- true
854
- end
855
-
856
- # TODO: rewrite evolution file exists check for speed
857
- check_evolution_number
858
-
859
- @db
860
- end
821
+ # Adds debug message to inspect object. Debug messages will be shown at top of
822
+ # application response body.
823
+ def debug(object, context = Kernel.caller[0])
824
+ @debug_messages.push( [object.inspect, context] )
825
+ object
861
826
  end
827
+ alias_method :trace, :debug
862
828
 
863
- # Kiss Model cache, used to invoke and store Kiss database models.
864
- #
865
- # Example:
866
- # models[:users] : database model for `users' table
867
- def dbm
868
- db unless @db
869
- @dbm ||= Kiss::ModelCache.new(self)
829
+ # Starts a new benchmark timer, with optional label. Benchmark results will be shown
830
+ # at top of application response body.
831
+ def bench(label = nil, context = Kernel.caller[0])
832
+ stop_benchmark(context)
833
+ @benchmarks.push(
834
+ :label => label,
835
+ :start_time => Time.now,
836
+ :start_context => context
837
+ )
870
838
  end
871
- alias_method :models, :dbm
872
839
 
873
- def random_text(*args)
874
- self.class.random_text(*args)
840
+ # Stops last benchmark timer, if still running.
841
+ def stop_benchmark(end_context = nil)
842
+ if @benchmarks[-1] && !@benchmarks[-1][:end_time]
843
+ @benchmarks[-1][:end_time] = Time.now
844
+ @benchmarks[-1][:end_context] = end_context
845
+ end
875
846
  end
876
847
 
877
- def h(*args)
878
- self.class.h(*args)
848
+
849
+ ##### OTHER METHODS #####
850
+
851
+ # Returns URL/URI of app root (corresponding to top level of action_dir).
852
+ def app_url(options = {})
853
+ options.empty? ? (@app_url ||= @protocol + '://' + @app_host + @app_uri) :
854
+ (options[:protocol] || @protocol) + '://' + (options[:host] || @app_host) +
855
+ (options[:uri] || @app_uri)
879
856
  end
880
857
 
881
- def template
882
- @template || @action
858
+ # Returns URL/URI of app's static assets (asset_host or public_uri).
859
+ def pub_url(options = {})
860
+ options.empty? ?
861
+ (@pub ||= (@@asset_host ? @protocol + '://' + @@asset_host : '') + @@asset_uri) :
862
+ (options[:protocol] || @protocol) + '://' + (options[:host] || @@asset_host) +
863
+ (options[:uri] || @@asset_uri)
883
864
  end
865
+ alias_method :asset_url, :pub_url
884
866
 
885
867
  # Adds data to be displayed in "Cache" section of Kiss exception reports.
886
- def set_exception_cache(data)
887
- @exception_cache.merge!(data)
868
+ def exception_cache(data = nil)
869
+ @exception_cache.merge!(data) if data
870
+ @exception_cache
871
+ end
872
+ alias_method :set_exception_cache, :exception_cache
873
+
874
+ # Returns new Kiss::Mailer object using specified options.
875
+ def new_email(options = {})
876
+ mailer = Kiss::Mailer.new(options)
877
+ mailer.controller = self
878
+ mailer
888
879
  end
889
880
  end