kiss 1.1 → 1.7

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.
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