kiss 1.1 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/LICENSE +1 -1
  2. data/Rakefile +2 -1
  3. data/VERSION +1 -1
  4. data/bin/kiss +151 -34
  5. data/data/scaffold.tgz +0 -0
  6. data/lib/kiss.rb +389 -742
  7. data/lib/kiss/accessors/controller.rb +47 -0
  8. data/lib/kiss/accessors/request.rb +106 -0
  9. data/lib/kiss/accessors/template.rb +23 -0
  10. data/lib/kiss/action.rb +502 -132
  11. data/lib/kiss/bench.rb +14 -5
  12. data/lib/kiss/debug.rb +14 -6
  13. data/lib/kiss/exception_report.rb +22 -299
  14. data/lib/kiss/ext/core.rb +700 -0
  15. data/lib/kiss/ext/rack.rb +33 -0
  16. data/lib/kiss/ext/sequel_database.rb +47 -0
  17. data/lib/kiss/ext/sequel_mysql_dataset.rb +23 -0
  18. data/lib/kiss/form.rb +404 -179
  19. data/lib/kiss/form/field.rb +183 -307
  20. data/lib/kiss/form/field_types.rb +239 -0
  21. data/lib/kiss/format.rb +88 -70
  22. data/lib/kiss/html/exception_report.css +222 -0
  23. data/lib/kiss/html/exception_report.html +210 -0
  24. data/lib/kiss/iterator.rb +14 -12
  25. data/lib/kiss/login.rb +8 -8
  26. data/lib/kiss/mailer.rb +68 -66
  27. data/lib/kiss/model.rb +323 -36
  28. data/lib/kiss/rack/bench.rb +16 -8
  29. data/lib/kiss/rack/email_errors.rb +25 -15
  30. data/lib/kiss/rack/errors_ok.rb +2 -2
  31. data/lib/kiss/rack/facebook.rb +6 -6
  32. data/lib/kiss/rack/file_not_found.rb +10 -8
  33. data/lib/kiss/rack/log_exceptions.rb +3 -3
  34. data/lib/kiss/rack/recorder.rb +2 -2
  35. data/lib/kiss/rack/show_debug.rb +2 -2
  36. data/lib/kiss/rack/show_exceptions.rb +2 -2
  37. data/lib/kiss/request.rb +435 -0
  38. data/lib/kiss/sequel_session.rb +15 -14
  39. data/lib/kiss/static_file.rb +20 -13
  40. data/lib/kiss/template.rb +327 -0
  41. metadata +60 -25
  42. data/lib/kiss/controller_accessors.rb +0 -81
  43. data/lib/kiss/hacks.rb +0 -188
  44. data/lib/kiss/sequel_mysql.rb +0 -25
  45. data/lib/kiss/template_methods.rb +0 -167
@@ -0,0 +1,47 @@
1
+ class Kiss
2
+ module KissAccessors
3
+ # Escapes string for use in URLs.
4
+ def url_escape(string)
5
+ string.url_escape
6
+ end
7
+ alias_method :u, :url_escape
8
+ alias_method :escape, :url_escape
9
+ alias_method :url_encode, :url_escape
10
+ alias_method :escape_url, :url_escape
11
+
12
+ # Encodes string for output to HTML.
13
+ def html_escape(string)
14
+ string.html_escape
15
+ end
16
+ alias_method :h, :html_escape
17
+ alias_method :escape_html, :html_escape
18
+ end
19
+
20
+ module ControllerAccessors
21
+ def environment
22
+ controller.environment
23
+ end
24
+
25
+ def public_dir
26
+ controller.public_dir
27
+ end
28
+
29
+ def upload_dir
30
+ controller.upload_dir
31
+ end
32
+
33
+ def database
34
+ controller.database
35
+ end
36
+
37
+ def models
38
+ controller.models
39
+ end
40
+ alias_method :dbm, :models
41
+
42
+
43
+ # request data
44
+
45
+ include KissAccessors
46
+ end
47
+ end
@@ -0,0 +1,106 @@
1
+ class Kiss
2
+ module RequestAccessors
3
+ def models
4
+ request.models
5
+ end
6
+ alias_method :dbm, :models
7
+
8
+ def file_cache(*args, &block)
9
+ request.file_cache(*args, &block)
10
+ end
11
+
12
+ def session
13
+ request.session
14
+ end
15
+
16
+ def login
17
+ request.login
18
+ end
19
+
20
+ def redirect_url(*args)
21
+ request.redirect_url(*args)
22
+ end
23
+
24
+ def redirect_action(*args)
25
+ request.redirect_action(*args)
26
+ end
27
+
28
+ def app(*args)
29
+ request.app_url(*args)
30
+ end
31
+
32
+ def pub(*args)
33
+ request.pub_url(*args)
34
+ end
35
+
36
+ def send_file(*args)
37
+ request.send_file(*args)
38
+ end
39
+
40
+ def send_response(*args)
41
+ request.send_response(*args)
42
+ end
43
+
44
+ def env
45
+ request.env
46
+ end
47
+
48
+ def protocol
49
+ request.protocol
50
+ end
51
+
52
+ def host
53
+ request.host
54
+ end
55
+
56
+ def path_info
57
+ request.path_info
58
+ end
59
+
60
+ def query_string_with_params
61
+ request.query_string_with_params
62
+ end
63
+
64
+ def request_url_with_params
65
+ request.request_url_with_params
66
+ end
67
+
68
+ def set_exception_cache(*args)
69
+ request.set_exception_cache(*args)
70
+ end
71
+
72
+ def debug(object)
73
+ request.debug(object, Kernel.caller[0])
74
+ end
75
+ alias_method :trace, :debug
76
+
77
+ def start_benchmark(label = nil)
78
+ request.start_benchmark(label, Kernel.caller[0])
79
+ end
80
+ alias_method :bench, :start_benchmark
81
+
82
+ def stop_benchmark
83
+ request.stop_benchmark(Kernel.caller[0])
84
+ end
85
+ alias_method :bench_stop, :stop_benchmark
86
+
87
+ def response
88
+ request.response
89
+ end
90
+
91
+ def cookies
92
+ request.cookies
93
+ end
94
+
95
+ def set_cookie(*args)
96
+ response.set_cookie(*args)
97
+ end
98
+ end
99
+
100
+ module DatabaseAccessors
101
+ def database
102
+ request.database
103
+ end
104
+ alias_method :db, :database
105
+ end
106
+ end
@@ -0,0 +1,23 @@
1
+ class Kiss
2
+ module TemplateMethods
3
+ # Contains data set by action/mailer logic to be displayed in templates.
4
+ def data
5
+ @data
6
+ end
7
+ alias_method :vars, :data
8
+
9
+ def data=(hash)
10
+ @data = hash
11
+ end
12
+ alias_method :'vars=', :'data='
13
+
14
+ # Merges specified data (key-value pairs) into template data hash
15
+ # (provided for backward-compatibility with apps pre-1.2).
16
+ def set(vars)
17
+ vars.each_pair do |key, value|
18
+ @data[key] = value
19
+ self.instance_variable_set(:"@#{key}", value)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,78 +1,327 @@
1
1
  class Kiss
2
2
  class Action
3
+ @@template_class = nil
4
+
5
+ cattr_reader :class_unique, :template_class
6
+
3
7
  class << self
8
+ _attr_reader :subdir, :part, :name, :action_path, :parent_class, :object_options, :aliases
9
+ attr_reader :layout, :breadcrumbs, :authentication_required, :base_url
10
+
11
+ dsl_accessor :breadcrumb, :object_breadcrumb
12
+
4
13
  def inherited(subclass)
5
- subclass.clear_subclasses
14
+ subclass.init_child_classes
6
15
  end
7
- def clear_subclasses
8
- @subclasses = {}
16
+ def init_child_classes
17
+ @_child_classes = {}
18
+ end
19
+
20
+ def get_subclass_from_path(path, request, params)
21
+ action_subdir = ''
22
+ action_path = '/'
23
+ action_found = false
24
+ args = []
25
+ objects = {}
26
+ breadcrumbs = []
27
+ object = nil
28
+ object_part = nil
29
+ collection = nil
30
+
31
+ request.redirect_action('/') if path == ''
32
+
33
+ action_class = self.get_child_class
34
+
35
+ parts = path.sub(/\A\/*/, '').split(/\/+/)
36
+ parts.push('') if path[-1, 1] == '/'
37
+
38
+ while part = parts.shift
39
+ action_path += part
40
+ part = (object_part ? 'view' : @@controller.default_action) if part.empty?
41
+
42
+ # =column syntax not yet supported
43
+ # when adding this support, be sure to change Action#redirect_action also
44
+
45
+ if part =~ /\A([\w\-\.\+\%]+)\=([\w\-\.\+\%]+)\Z/
46
+ params[$1.url_unescape] = $2.url_unescape
47
+ action_path += '/'
48
+ next
49
+ end
50
+
51
+ if part !~ /\A[\w\-\.]*\Z/i
52
+ raise Kiss::FileNotFoundError::InvalidAction, 'invalid action path'
53
+ end
54
+
55
+ if redirect = action_class.aliases[part]
56
+ action, options = redirect
57
+ request.redirect_action(
58
+ absolute_uri( action + '/' + parts.join('/'), action_path ),
59
+ options
60
+ )
61
+ end
62
+
63
+ test_path = @@controller.action_dir + action_subdir + '/' + part
64
+ if @@controller.directory_exists?(test_path)
65
+ action_subdir += '/' + part
66
+ action_class = action_class.get_child_class(part, true)
67
+ object = object_part = nil
68
+
69
+ collection = action_class.breadcrumb
70
+
71
+ action_path += '/'
72
+ if action_class.breadcrumbs.empty?
73
+ if File.file?("#{@@controller.action_dir}#{action_class.name}index.rb") ||
74
+ File.file?("#{@@controller.action_dir}#{action_class.name}index.rhtml")
75
+ breadcrumbs << [action_class.breadcrumb, action_path]
76
+ end
77
+ else
78
+ breadcrumbs.push(*action_class.breadcrumbs)
79
+ end
80
+ else
81
+ # remove .rb extension, if any
82
+ part.sub!(/(\.rb)+\Z/, '')
83
+
84
+ # does part have an extension?
85
+ if part =~ /\A(.+)\.(\w+)\Z/
86
+ extension = $2
87
+ part = $1
88
+ else
89
+ extension = 'rhtml'
90
+ end
91
+
92
+ test_path = @@controller.action_dir + action_subdir + '/' + part
93
+ if File.file?("#{test_path}.rb") || File.file?("#{test_path}.#{(extension == 'xls') ? 'txt' : extension}")
94
+ action_found = true
95
+ action_class = action_class.get_child_class(part)
96
+ break
97
+ else
98
+ unless part =~ /\A[\w\.\-]+\Z/
99
+ raise Kiss::FileNotFoundError, "invalid URI"
100
+ end
101
+
102
+ object_options = action_class.object_options
103
+ if object_options && (object_options[:column] != :id || part =~ /\A\d+\Z/)
104
+ object_part = part
105
+ object = request.dbm[object_options[:class_name]].find(object_options[:column] => object_part)
106
+ objects[object_options[:variable_name]] = object
107
+
108
+ action_path += '/'
109
+
110
+ if !object
111
+ raise Kiss::FileNotFoundError, "resource '#{test_path.html_escape}' not found"
112
+ end
113
+
114
+ object_breadcrumb = action_class.object_breadcrumb
115
+ object_display_name = object[object.class.display_column] ||
116
+ (object.send(object.class.display_column) rescue nil)
117
+ if (object_breadcrumb != false)
118
+ breadcrumbs << [
119
+ (object_breadcrumb.is_a?(Proc) ? object_breadcrumb.call(object) : object_breadcrumb) ||
120
+ object_display_name,
121
+ action_path
122
+ ]
123
+ end
124
+
125
+ next
126
+ else
127
+ if part == 'view'
128
+ request.redirect_action(path + '..')
129
+ end
130
+ raise Kiss::FileNotFoundError, "resource '#{test_path.html_escape}' not found"
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ # if no action found, add a trailing slash and try again
137
+ request.redirect_action(path + '/') unless action_found
138
+
139
+ # keep rest of path_info in args
140
+ args.push(*parts)
141
+
142
+ action = action_class.new(request, action_path, extension, collection, object, object_part, object_display_name, breadcrumbs, args, params)
143
+ objects.each_pair do |key, value|
144
+ action.instance_variable_set(key, value)
145
+ end
146
+ action
9
147
  end
10
148
 
11
- def get_subclass(key, path, controller)
149
+ def get_root_class(controller, root_dir)
150
+ parent = self
151
+ klass = Class.new(self)
152
+ Kiss.register_class_path(klass, root_dir)
153
+ klass.class_eval do
154
+ @@action_dir = root_dir
155
+ @@controller = controller
156
+ @@root_class = klass
157
+
158
+ @_parent_class = parent
159
+ @_subdir = ''
160
+
161
+ @layout = nil
162
+ @breadcrumbs = []
163
+ @authentication_required = @@controller.authenticate_all ? true : false
164
+
165
+ @_aliases = {}
166
+ end
167
+ klass
168
+ end
169
+
170
+ def get_child_class(part = nil, is_directory = !part)
12
171
  # each subdir must get a subclass of the parent dir class;
13
- # each subclass has its own @subclasses hash of its subdirs
172
+ # each subclass has its own @_child_classes hash of its subdirs
173
+
174
+ subdir = self.name.to_s.sub(/\/\Z/, '')
175
+ name = self.name.to_s + part.to_s + (is_directory ? '/' : '')
176
+ path = name + (is_directory ? '_action' : '') + '.rb'
177
+ parent = self
178
+
179
+ layout = @layout
14
180
 
15
- src, updated = controller.file_cache(path, true)
181
+ auth_path = name
182
+ auth_req = @authentication_required && !@@controller.authenticate_exclude.include?(auth_path)
16
183
 
17
- if updated
184
+ src, updated = @@controller.file_cache(@@action_dir + path, true)
185
+
186
+ if updated || !@_child_classes.has_key?(part)
18
187
  klass = Class.new(self)
19
- klass.class_eval(src,path) if src
20
- @subclasses[key] = klass
188
+ Kiss.register_class_path(klass, @@action_dir + path)
189
+ klass.class_eval do
190
+ @_subdir = subdir
191
+ @_part = part
192
+ @_name = name
193
+ @_action_path = path
194
+ @_parent_class = parent
195
+ @_breadcrumb = part ? part.titleize : nil
196
+
197
+ @layout = layout
198
+ @breadcrumbs = []
199
+ @authentication_required = auth_req
200
+
201
+ @_aliases = {}
202
+
203
+ selects_object if part && is_directory
204
+ end
205
+
206
+ if src
207
+ klass.class_eval(src, @@action_dir + path)
208
+ end
209
+ @_child_classes[part] = klass
21
210
  end
22
211
 
23
- @subclasses[key]
212
+ @_child_classes[part]
213
+ end
214
+
215
+ def selects_object(options = {})
216
+ if options.has_key?(:variable_name)
217
+ variable_name = options[:variable_name]
218
+ raise 'variable name must be a symbol' unless variable_name.is_a?(Symbol)
219
+ raise 'variable name must start with @' unless variable_name.to_s[0, 1] == '@'
220
+ end
221
+
222
+ (@_object_options ||= {
223
+ :class_name => @_part.pluralize.to_sym,
224
+ :variable_name => ('@'+@_part.singularize).to_sym,
225
+ :column => :id
226
+ }).merge!(options)
227
+ end
228
+ alias_method :select_object, :selects_object
229
+ alias_method :accepts_object, :selects_object
230
+ alias_method :accept_object, :selects_object
231
+
232
+ def alias_action(part, action, options = {})
233
+ @_aliases[part.to_s] = [action.to_s, options]
234
+ end
235
+
236
+ def absolute_uri(path, action_path)
237
+ # need to add something to action_path before File.dirname, in case
238
+ # action_path ends in /
239
+ path =~ /\A\// ? path : action_path.gsub(/[^\/]*\Z/, '') + path
24
240
  end
25
241
  end
26
- self.clear_subclasses
27
-
242
+ self.init_child_classes
28
243
 
244
+ include Kiss::ControllerAccessors
245
+ include Kiss::RequestAccessors
246
+ include Kiss::DatabaseAccessors
29
247
  include Kiss::TemplateMethods
30
248
 
31
- attr_reader :controller, :forms, :action, :action_uri, :action_subdir, :template, :extension,
32
- :args, :params, :output, :output_options
249
+ attr_accessor :layout, :title
250
+
251
+ _attr_reader :request, :controller, :forms, :action, :action_path, :template,
252
+ :extension, :args, :params, :output, :output_options
33
253
 
34
254
  def arg(index)
35
- @args[index]
255
+ @_args[index]
36
256
  end
37
257
 
38
258
  def args
39
- @args
259
+ @_args
40
260
  end
41
261
 
42
262
  # Raises custom error message for missing methods, specifying current action
43
263
  # more clearly than standard error message.
44
- def method_missing(meth)
45
- raise NoMethodError, "undefined method `#{meth}'"
264
+ def method_missing(method, *args, &block)
265
+ if method.to_s =~ /\A_class_(\d+)_(.*)/
266
+ __send__ $2.to_sym, $1.to_i, *args, &block
267
+ else
268
+ raise NoMethodError, "undefined method `#{method}'"
269
+ end
46
270
  end
47
271
 
48
272
  # Creates a new action instance from controller data.
49
- def initialize(controller,action,action_uri,action_subdir,extension,args,params = nil)
50
- @controller = controller
51
- @action = @template = action
52
- @action_uri = action_uri
53
- @action_subdir = action_subdir
54
- @extension = extension
55
- @args = args
56
- @params = params || {}
273
+ def initialize(request, action_path, extension, collection, object, object_part, object_display_name, breadcrumbs, args, params = {})
274
+ @_request = request
275
+ @_controller = request.controller
276
+ @_action_path = action_path
277
+ @_extension = extension
278
+ @_object = object
279
+ @_object_part = object_part
280
+ @_args = args
281
+ @_params = params
282
+
283
+ @_template = self.class.name
284
+
285
+ @_arg_index = -1
286
+ @_forms = {}
57
287
 
58
288
  @data = {}
59
- @forms = {}
60
- @template_dir = @controller.template_dir
289
+
290
+ @breadcrumbs = breadcrumbs
291
+ @title =
292
+ is_index? ? (@breadcrumbs.pop.first unless @breadcrumbs.empty?) :
293
+ is_view? ? (@breadcrumbs.pop; object_display_name) :
294
+ is_edit? ? "#{@_object ? 'Edit' : 'Add'} #{collection.first.singularize}" :
295
+ is_delete? ? "Delete #{collection.first.singularize}" : self.class.part.titlecase
296
+
297
+ @base_url = app + action_path
298
+
299
+ after_initialize
300
+ end
301
+
302
+ def breadcrumbs_html
303
+ @breadcrumbs.map do |crumb|
304
+ %Q(<a href="#{app + crumb[1]}">#{crumb[0]}</a> &gt;)
305
+ end.join(' ')
306
+ end
307
+
308
+ def context_class
309
+ Kiss.context_class
61
310
  end
62
311
 
63
312
  # Returns true is `form' param matches current action path.
64
313
  def form_action_match
65
- form = params['form']
66
- return false unless form =~ /^\/?\w+(\/\w+)*$/s
67
- return form == @action || @action =~ /\/#{form}/
314
+ return params['form'] == action_path
68
315
  end
69
316
 
70
317
  # Raises error if database model object does not match the key=value data in
71
318
  # desired_values hash.
72
- def check_desired_values(object,desired_values)
73
- desired_values.each_pair do |key,value|
74
- unless object[key] == value
75
- raise "#{object.table.to_s.singularize} id=#{object.id} does not have #{key} == #{value.inspect}"
319
+ def check_desired_values(object, desired_values = nil)
320
+ if desired_values
321
+ desired_values.each_pair do |key, value|
322
+ unless object[key] == value
323
+ raise "#{object.class.table.to_s.singularize} id=#{object.id} does not have #{key} == #{value.inspect}"
324
+ end
76
325
  end
77
326
  end
78
327
  object
@@ -80,90 +329,160 @@ class Kiss
80
329
 
81
330
  # Verifies valid id value, selects database model object from specified table
82
331
  # with specified id, and checks result to match desired_values.
83
- def get_object_by_id(table, id, desired_values = {})
332
+ def get_object_by_id(table, id, desired_values = nil)
84
333
  id = id.to_i
85
- raise 'bad object id' unless id > 0
86
- result = dbm[table][id] || (raise "#{table.to_s.singularize} id=#{id} not found")
87
- check_desired_values(result,desired_values)
334
+ result = (id > 0) ? dbm[table][id] : nil
335
+ raise "'#{table}' object not found" unless result
336
+
337
+ check_desired_values(result, desired_values)
88
338
  end
89
339
 
90
- # Returns database model object from specified table, with id matching argument
91
- # of specified index (defaults to first argument). Raises error if object not
92
- # found in database or object data does not match desired_values.
93
- def arg_object(table, index = 0, desired_values = {})
94
- get_object_by_id(table, arg(index).to_i, desired_values)
340
+ def get_object_by_value(table, value, column, *args)
341
+ desired_values = args.first
342
+ if column
343
+ result = dbm[table].where(column => value).first
344
+ raise "'#{table}' object not found" unless result
345
+
346
+ check_desired_values(result, desired_values)
347
+ else
348
+ get_object_by_id(table, value, desired_values)
349
+ end
350
+ end
351
+
352
+ # Returns database model object from specified table, with column matching
353
+ # argument of specified index (defaults to first argument). Raises error if
354
+ # object not found in database or object data does not match desired_values.
355
+ def arg_object(table, *args)
356
+ index = args.first.is_a?(Numeric) ? args.shift : 0
357
+ value = arg(index)
358
+
359
+ column = args.first.is_a?(Symbol) ? args.shift : nil
360
+
361
+ get_object_by_value(table, value, column, *args) || begin
362
+ raise Kiss::FileNotFoundError::Object, "could not find #{table.to_s.singularize} with #{column || 'id'}=#{value} (arg #{index})"
363
+ end
95
364
  end
96
365
 
97
366
  # Returns arg_object, or new object of specified table if arg_object fails.
98
- def arg_object_or_new(table,index = 0,desired_values = {})
99
- arg_object(table,index,desired_values) rescue dbm[table].new(desired_values)
367
+ def arg_object_or_new(table, *args)
368
+ begin
369
+ arg_object(table, *args)
370
+ rescue
371
+ # remove non-hash arguments, looking for desired_values
372
+ desired_values = args.last.is_a?(Hash) ? args.last : {}
373
+ dbm[table].new(desired_values)
374
+ end
375
+ end
376
+
377
+ def next_arg_index
378
+ @_arg_index += 1
379
+ end
380
+
381
+ def next_arg_object(table, *args)
382
+ arg_object(table, next_arg_index, *args)
100
383
  end
101
384
 
102
- # Returns database model object from specified table, with id matching param
385
+ def next_arg_object_or_new(table, *args)
386
+ arg_object_or_new(table, next_arg_index, *args)
387
+ end
388
+
389
+ # Returns database model object from specified table, with column matching param
103
390
  # of specified key (defaults to singular form of table name). Raises error if
104
391
  # object not found in database or object data does not match desired_values.
105
- def param_object(table, key = nil, desired_values = {})
392
+ def param_object(table, key = nil, column = nil, *args)
106
393
  key ||= table.to_s.singularize
107
- get_object_by_id(table, params[key.to_s], desired_values)
394
+ get_object_by_value(table, params[key.to_s], column, *args)
108
395
  end
109
396
 
110
397
  # Returns param_object, or new object of specified table if param_object fails.
111
- def param_object_or_new(table,key = nil,desired_values = {})
112
- param_object(table,key,desired_values) rescue dbm[table].new(desired_values)
398
+ def param_object_or_new(table, *args)
399
+ param_object(table, *args) rescue begin
400
+ desired_values = args.last.is_a?(Hash) ? args.last : {}
401
+ dbm[table].new(desired_values)
402
+ end
113
403
  end
114
404
 
115
405
  # Validates param of specified key against specified format.
116
- def validate_param(key, format, required = false)
117
- Kiss.validate_value(@params[key], format, required, "param '#{key}'")
406
+ def validate_param(key, format, options = {})
407
+ @_params[key.to_s].validate(format, options.merge(:label => "param '#{key}'"))
118
408
  end
119
409
 
120
410
  # Validates argument of specified index against specified format.
121
- def validate_arg(index, format, required = false)
122
- Kiss.validate_value(@args[index], format, required, "arg #{index}")
411
+ def validate_arg(format, index = 0, options = {})
412
+ @_args[index].validate(format, options.merge(:label => "arg #{index}"))
123
413
  end
124
414
 
125
- # Creates and invokes new Kiss::Mailer instance to send email message via sendmail.
126
- def sendmail(options = {})
127
- send_email({
128
- :engine => :sendmail
415
+ # Creates and invokes new Kiss::Mailer instance to send email message via SMTP.
416
+ def new_email(options = {})
417
+ request.new_email({
418
+ :data => data
129
419
  }.merge(options))
130
420
  end
131
-
132
- # Creates and invokes new Kiss::Mailer instance to send email message via SMTP.
421
+
133
422
  def send_email(options = {})
134
- Kiss::Mailer.new({
135
- :data => data,
136
- :controller => controller
137
- }.merge(options)).send
423
+ new_email(options).send
138
424
  end
139
-
140
- # Sends HTTP 302 response to redirect client agent to specified URL.
141
- def redirect_url(*args)
142
- controller.redirect_url(*args)
143
- end
144
-
425
+
145
426
  # Redirects to specified action path, which may also include arguments.
146
427
  def redirect_action(action, options = {})
147
- redirect_url( app(options) + app_absolute_path(action) + (options[:params] ?
148
- '?' + options[:params].keys.map do |k|
149
- "#{Kiss.url_escape k.to_s}=#{Kiss.url_escape options[:params][k].to_s}"
150
- end.join('&') : '')
151
- )
428
+ # need to rewrite this if current action class looks up objects
429
+ # by some other column than id
430
+ if options.has_key?(:object)
431
+ dest_object = options[:object]
432
+ column = self.class.parent_class.object_options[:column]
433
+ action = "#{@_object_part && '../'}#{"#{dest_object[column]}/" if dest_object}#{action}"
434
+ end
435
+ request.redirect_action( absolute_uri(action), options )
152
436
  end
153
437
 
154
- def invoke(path,options = {})
155
- action_handler = controller.invoke_action(path,options[:params] || {},{:layout => nil})
156
- action_handler.output
438
+ # Redirects to index of the same action group.
439
+ def redirect_index(options = {})
440
+ redirect_action('', options.merge(:object => nil))
157
441
  end
158
- alias_method :process_action, :invoke
159
-
160
- # Convert specified action path by prefixing current action_subdir,
442
+
443
+ # Redirects to the view action on the object.
444
+ def redirect_object(object = nil, options = {})
445
+ if object.is_a?(Hash)
446
+ options = object
447
+ object = nil
448
+ end
449
+ object ||= options[:object] || @_object
450
+ if object && !object.new?
451
+ redirect_action('', {:object => object}.merge(options))
452
+ else
453
+ redirect_index(options)
454
+ end
455
+ end
456
+ alias_method :redirect_view, :redirect_object
457
+
458
+ # Redirects to index of the same action group.
459
+ def redirect_pop(options = {})
460
+ if @breadcrumbs.empty?
461
+ redirect_index(options)
462
+ else
463
+ redirect_action(@breadcrumbs.last.last, options)
464
+ end
465
+ end
466
+ alias_method :redirect_breadcrumb, :redirect_pop
467
+
468
+ def render_action(path, options = {})
469
+ action = request.render_action(
470
+ absolute_uri(path),
471
+ options[:params] || {}, {:layout => nil}
472
+ )
473
+ action.output
474
+ end
475
+
476
+ # Convert specified action path by prefixing current action subdir,
161
477
  # unless specified path starts with slash (/).
162
478
  def app_absolute_path(path)
163
- path = @action_subdir + path if path !~ /\A\//
164
- path
479
+ path !~ /\A\// ? self.class.subdir + '/' + path : path
165
480
  end
166
-
481
+
482
+ def absolute_uri(relative_uri)
483
+ self.class.absolute_uri(relative_uri, action_path)
484
+ end
485
+
167
486
  # Return string result from rendering specified string or template options.
168
487
  def render_to_string(options)
169
488
  if options.is_a?(String)
@@ -172,35 +491,57 @@ class Kiss
172
491
  return options[:text].to_s
173
492
  end
174
493
 
175
- @base_url ||= app + action_uri
494
+ if options.is_a?(Hash)
495
+ @title = options[:title] if options.has_key?(:title)
496
+ @layout = options[:layout] if options.has_key?(:layout)
497
+ @breadcrumbs = options[:breadcrumbs] if options.has_key?(:breadcrumbs)
498
+ end
499
+
500
+ @@template_class ||= Kiss::Template.get_root_class(controller, controller.template_dir)
501
+
502
+ content = options[:content].is_a?(String) ? begin
503
+ template_class = @@template_class.get_subclass_from_path(self.class.subdir, 'rhtml')
504
+ options[:content]
505
+ end : begin
506
+ path = @@template_class.get_template_path(
507
+ {
508
+ :template => template,
509
+ :subdir => self.class.subdir,
510
+ :extension => (extension == 'xls') ? 'txt' : extension
511
+ }.merge(options)
512
+ )
513
+ template_class = @@template_class.get_subclass_from_path(path)
514
+ template_class.new(request, self).call
515
+ end
176
516
 
177
517
  unless defined?(@layout)
178
- @layout = options.is_a?(Hash) && options.has_key?(:layout) ? options[:layout] :
179
- (extension == 'rhtml' ? Kiss.layout : nil)
518
+ @layout = extension == 'rhtml' ? self.class.layout || (template_class && template_class.layout) || '/_layout.rhtml' : nil
180
519
  end
181
- content = options[:content].is_a?(String) ? options[:content] : process({
182
- :template => template,
183
- :extension => extension
184
- }.merge(options))
185
520
 
521
+ i = 0
186
522
  while @layout
187
- layout_path = get_template_path(
523
+ ext = options[:extension] || extension
524
+ layout_path = @@template_class.get_template_path(
188
525
  :template => @layout,
189
- :extension => options[:extension] || extension
526
+ :subdir => self.class.subdir,
527
+ :extension => (ext == 'xls') ? 'txt' : ext
190
528
  ) rescue break
191
-
192
- # clear layout
193
- # (however, layout template is allowed to set @layout to something else)
194
- @layout = nil
529
+
530
+ # cache layout for comparison after render
531
+ prev_layout = @layout
195
532
 
196
533
  # process layout file
197
- content = erubis(layout_path,binding,content)
534
+ layout_class = @@template_class.get_subclass_from_path(layout_path)
535
+ content = layout_class.new(request, self).call(content)
536
+
537
+ if @layout == prev_layout
538
+ @layout = layout_class.parent_class.parent_class.layout
539
+ end
198
540
  end
199
- # add a base tag in case of arguments appended to request URI
200
541
 
201
- base_url = options.has_key?(:base_url) ? options[:base_url] : @base_url
202
- if (base_url)
203
- content = Kiss.html_prepend(%Q(<base href="#{base_url}" />),content,'head')
542
+ # add a base tag in case of arguments appended to request URI
543
+ if extension == 'rhtml' && (base_url = options.has_key?(:base_url) ? options[:base_url] : @base_url)
544
+ content = content.prepend_html(%Q(<base href="#{base_url}" />), 'head')
204
545
  end
205
546
 
206
547
  content
@@ -208,60 +549,89 @@ class Kiss
208
549
 
209
550
  # Render and return response to Rack.
210
551
  def render(options = {})
211
- @output = render_to_string(options)
212
- @output_options = options
552
+ Dir.chdir(controller.project_dir)
553
+
554
+ @_output = render_to_string(options)
555
+ @_output_options = options
213
556
  throw :kiss_action_done
214
557
  end
215
-
216
- # Placeholder for generic actions that do nothing but render template.
217
- # render is called from Kiss#call after this method returns.
218
- def call; end
219
558
 
220
559
  # Does nothing by default; override this method in your _action.rb or
221
560
  # other action files to specify authentication behavior.
222
561
  def authenticate; end
223
562
 
224
- # Does nothing by default; override this method in your _action.rb or
225
- # other action files to expand objects from persistent login data.
226
- def expand_login; end
563
+ # Callback method: called after action handler initialized. Does nothing
564
+ # by default; override this method in your _action.rb or other action files.
565
+ def after_initialize; end
566
+
567
+ # Callback method: called before action call. Does nothing by default;
568
+ # override this method in your _action.rb or other action files.
569
+ def before_call; end
570
+
571
+ # Placeholder for generic actions that do nothing but render template.
572
+ # render is called from Kiss#call after this method returns.
573
+ def call; end
574
+
575
+ # Callback method: called after action call. Does nothing by default;
576
+ # override this method in your _action.rb or other action files.
577
+ def after_call; end
227
578
 
228
579
  # Ensure that action requested via SSL; redirects to same action with
229
580
  # https protocol if protocol is not https.
230
581
  def force_ssl(options = {})
231
582
  unless protocol == 'https'
232
- redirect_action(@action, options.merge( :protocol => 'https' ))
583
+ redirect_action(request.path_with_query_string, options.merge( :protocol => 'https' ))
233
584
  end
234
585
  end
235
586
  alias_method :force_https, :force_ssl
587
+
588
+ def is_index?
589
+ self.class.name == self.class.subdir + '/index'
590
+ end
591
+
592
+ def is_view?
593
+ self.class.name == self.class.subdir + '/view'
594
+ end
595
+
596
+ def is_edit?
597
+ self.class.name == self.class.subdir + '/edit'
598
+ end
599
+
600
+ def is_delete?
601
+ self.class.name == self.class.subdir + '/delete'
602
+ end
236
603
 
237
604
  # Creates and adds form to current action, using specified attributes.
238
- def add_form(attrs = {})
239
- # make sure form not already defined for this request
240
- @forms ||= {}
241
- attrs[:name] ||= @action
242
- form_name = attrs[:name]
243
- raise "page contains multiple forms named '#{form_name}'" if @forms.has_key?(form_name)
244
-
605
+ def add_form(attrs = {}, &block)
245
606
  # create form
246
- uri = app + request.path_info
247
- @forms[form_name] = @form = Kiss::Form.new({
248
- :action => uri,
249
- :request => self
250
- }.merge(attrs))
607
+ temp_form = Kiss::Form.new({
608
+ :name => attrs[:action] || action_path,
609
+ :action => app + request.path_with_query_string,
610
+ :delegate => self
611
+ }.merge(attrs), &block)
612
+
613
+ # make sure form not already defined for this request
614
+ @_forms ||= {}
615
+ raise "page contains multiple forms named '#{temp_form.name}'" if @_forms.has_key?(temp_form.name)
616
+ @_forms[temp_form.name] = @_form = temp_form
251
617
 
252
- # add data from controller to form
253
- if params['form'] == @form.name
254
- @form.submitted = true
255
- @form.params = params
618
+ # add data from request to form
619
+ if params['form'] == @_form.name
620
+ @_form.submitted = true
621
+ @_form.params = params
256
622
  end
257
623
 
258
- @form
624
+ @_form
259
625
  end
260
-
626
+
261
627
  # Returns most recently added form.
262
628
  # Shorthand for forms[0] when there is only one form.
263
629
  def form
264
- @form
630
+ @_form
631
+ end
632
+
633
+ def invoke_action(path, params = {})
634
+ request.invoke_action(path, params).output
265
635
  end
266
636
  end
267
637
  end