merb 0.3.4 → 0.3.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 (62) hide show
  1. data/README +206 -197
  2. data/Rakefile +12 -21
  3. data/bin/merb +1 -1
  4. data/examples/skeleton/Rakefile +6 -20
  5. data/examples/skeleton/dist/app/mailers/layout/application.erb +1 -0
  6. data/examples/skeleton/dist/conf/database.yml +23 -0
  7. data/examples/skeleton/dist/conf/environments/development.rb +1 -0
  8. data/examples/skeleton/dist/conf/environments/production.rb +1 -0
  9. data/examples/skeleton/dist/conf/environments/test.rb +1 -0
  10. data/examples/skeleton/dist/conf/merb.yml +32 -28
  11. data/examples/skeleton/dist/conf/merb_init.rb +16 -13
  12. data/examples/skeleton/dist/conf/router.rb +9 -9
  13. data/examples/skeleton/dist/schema/migrations/001_add_sessions_table.rb +2 -2
  14. data/lib/merb.rb +23 -18
  15. data/lib/merb/caching/fragment_cache.rb +3 -7
  16. data/lib/merb/caching/store/memcache.rb +20 -0
  17. data/lib/merb/core_ext/merb_array.rb +0 -0
  18. data/lib/merb/core_ext/merb_class.rb +44 -4
  19. data/lib/merb/core_ext/merb_enumerable.rb +43 -1
  20. data/lib/merb/core_ext/merb_hash.rb +200 -122
  21. data/lib/merb/core_ext/merb_kernel.rb +2 -0
  22. data/lib/merb/core_ext/merb_module.rb +41 -0
  23. data/lib/merb/core_ext/merb_numeric.rb +57 -5
  24. data/lib/merb/core_ext/merb_object.rb +172 -6
  25. data/lib/merb/generators/merb_app/merb_app.rb +15 -9
  26. data/lib/merb/merb_abstract_controller.rb +193 -0
  27. data/lib/merb/merb_constants.rb +26 -1
  28. data/lib/merb/merb_controller.rb +143 -234
  29. data/lib/merb/merb_dispatcher.rb +28 -20
  30. data/lib/merb/merb_drb_server.rb +2 -3
  31. data/lib/merb/merb_exceptions.rb +194 -49
  32. data/lib/merb/merb_handler.rb +34 -26
  33. data/lib/merb/merb_mail_controller.rb +200 -0
  34. data/lib/merb/merb_mailer.rb +33 -13
  35. data/lib/merb/merb_part_controller.rb +42 -0
  36. data/lib/merb/merb_plugins.rb +293 -0
  37. data/lib/merb/merb_request.rb +6 -4
  38. data/lib/merb/merb_router.rb +99 -65
  39. data/lib/merb/merb_server.rb +65 -21
  40. data/lib/merb/merb_upload_handler.rb +2 -1
  41. data/lib/merb/merb_view_context.rb +36 -15
  42. data/lib/merb/mixins/basic_authentication_mixin.rb +5 -5
  43. data/lib/merb/mixins/controller_mixin.rb +67 -28
  44. data/lib/merb/mixins/erubis_capture_mixin.rb +1 -8
  45. data/lib/merb/mixins/form_control_mixin.rb +280 -42
  46. data/lib/merb/mixins/render_mixin.rb +127 -45
  47. data/lib/merb/mixins/responder_mixin.rb +5 -7
  48. data/lib/merb/mixins/view_context_mixin.rb +260 -94
  49. data/lib/merb/session.rb +23 -0
  50. data/lib/merb/session/merb_ar_session.rb +28 -16
  51. data/lib/merb/session/merb_mem_cache_session.rb +108 -0
  52. data/lib/merb/session/merb_memory_session.rb +65 -20
  53. data/lib/merb/template/erubis.rb +22 -13
  54. data/lib/merb/template/haml.rb +5 -16
  55. data/lib/merb/template/markaby.rb +5 -3
  56. data/lib/merb/template/xml_builder.rb +17 -5
  57. data/lib/merb/test/merb_fake_request.rb +63 -0
  58. data/lib/merb/test/merb_multipart.rb +58 -0
  59. data/lib/tasks/db.rake +2 -0
  60. data/lib/tasks/merb.rake +20 -8
  61. metadata +24 -25
  62. data/examples/skeleton.tar +0 -0
@@ -1,6 +1,7 @@
1
1
  module Mongrel
2
2
  module Const
3
3
  POST = 'POST'.freeze unless const_defined?(:POST)
4
+ PUT = 'PUT'.freeze unless const_defined?(:PUT)
4
5
  QUERY_STRING = 'QUERY_STRING'.freeze unless const_defined?(:QUERY_STRING)
5
6
  UPLOAD_ID = 'upload_id'.freeze
6
7
  end
@@ -72,7 +73,7 @@ class MerbUploadHandler < Mongrel::HttpHandler
72
73
 
73
74
  def valid_upload?(params)
74
75
  @path_info.any? { |p| params[Mongrel::Const::PATH_INFO].include?(p) } &&
75
- params[Mongrel::Const::REQUEST_METHOD] == Mongrel::Const::POST &&
76
+ [Mongrel::Const::POST, Mongrel::Const::PUT].include?(params[Mongrel::Const::REQUEST_METHOD]) &&
76
77
  Mongrel::HttpRequest.query_parse(params[Mongrel::Const::QUERY_STRING])[Mongrel::Const::UPLOAD_ID]
77
78
  end
78
79
  end
@@ -6,26 +6,23 @@ module Merb
6
6
  PROTECTED_IVARS = %w[@_new_cookie
7
7
  @method
8
8
  @env
9
- @body
9
+ @controller
10
+ @_body
10
11
  @_fingerprint_before
11
- @pager
12
- @session
13
- @headers
14
- @page
15
- @cookies
16
- @request
17
- @status
12
+ @_session
13
+ @_headers
14
+ @_cookies
15
+ @_request
16
+ @_status
18
17
  @_view_context_cache
19
- @response
20
- @params]
18
+ @_response
19
+ @_params]
21
20
 
22
21
  module GlobalHelper
23
22
  end
24
- # the ViewContext is really
25
- # just an empty container for us to fill with instance
26
- # variables from the controller, include helpers into
27
- # and then use as the context object passed to Erubis
28
- # when evaluating the templates.
23
+ # The ViewContext is really just an empty container for us to fill with
24
+ # instance variables from the controller, include helpers into and then use as
25
+ # the context object passed to Erubis when evaluating the templates.
29
26
  class ViewContext
30
27
  include Merb::ViewContextMixin
31
28
  include Merb::FormControls
@@ -53,6 +50,30 @@ module Merb
53
50
  def controller
54
51
  @controller
55
52
  end
53
+
54
+ def request
55
+ @controller.request
56
+ end
57
+
58
+ def params
59
+ @controller.params
60
+ end
61
+
62
+ def cookies
63
+ @controller.cookies
64
+ end
65
+
66
+ def headers
67
+ @controller.headers
68
+ end
69
+
70
+ def session
71
+ @controller.session
72
+ end
73
+
74
+ def response
75
+ @controller.response
76
+ end
56
77
 
57
78
  alias_method :old_respond_to?, :respond_to?
58
79
 
@@ -6,7 +6,7 @@ module Merb
6
6
  def credentials
7
7
  if d = %w{REDIRECT_X_HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION
8
8
  X-HTTP_AUTHORIZATION HTTP_AUTHORIZATION}.
9
- inject([]) { |d,h| @env.has_key?(h) ? @env[h].to_s.split : d }
9
+ inject([]) { |d,h| request.env.has_key?(h) ? request.env[h].to_s.split : d }
10
10
  return Base64.decode64(d[1]).split(':')[0..1] if d[0] == 'Basic'
11
11
  end
12
12
  end
@@ -23,10 +23,10 @@ module Merb
23
23
  end
24
24
 
25
25
  def access_denied
26
- @status = 401
27
- @headers['Content-type'] = 'text/plain'
28
- @headers['Status'] = 'Unauthorized'
29
- @headers['WWW-Authenticate'] = "Basic realm=\"#{Merb::Server.basic_auth[:domain]}\""
26
+ set_status(401)
27
+ headers['Content-type'] = 'text/plain'
28
+ headers['Status'] = 'Unauthorized'
29
+ headers['WWW-Authenticate'] = "Basic realm=\"#{Merb::Server.basic_auth[:domain]}\""
30
30
  return 'Unauthorized'
31
31
  end
32
32
 
@@ -1,15 +1,59 @@
1
1
  module Merb
2
2
  module ControllerMixin
3
+
4
+ # Returns a URL according to the defined route. Accepts the path and
5
+ # an options hash. The path specifies the route requested. The options
6
+ # hash fills in the dynamic parts of the route.
7
+ #
8
+ # Nested resources such as:
9
+ # r.resources :blogposts do |post|
10
+ # post.resources :comments
11
+ # end
12
+ #
13
+ # Provide the following routes:
14
+ # [:blogposts, "/blogposts"]
15
+ # [:blogpost, "/blogposts/:id"]
16
+ # [:edit_blogpost, "/blogposts/:id/edit"]
17
+ # [:new_blogpost, "/blogposts/new"]
18
+ # [:custom_new_blogpost, "/blogposts/new/:action"]
19
+ # [:comments, "/blogposts/:blogpost_id/comments"]
20
+ # [:comment, "/blogposts/:blogpost_id/comments/:id"]
21
+ # [:edit_comment, "/blogposts/:blogpost_id/comments/:id/edit"]
22
+ # [:new_comment, "/blogposts/:blogpost_id/comments/new"]
23
+ # [:custom_new_comment, "/blogposts/:blogpost_id/comments/new/:action"]
24
+ #
25
+ # Examples:
26
+ #
27
+ # @post = Post.find(1)
28
+ # @comment = @post.comments.find(1)
29
+ #
30
+ # url(:blogposts) # => /blogposts
31
+ # url(:new_post) # => /blogposts/new
32
+ # url(:blogpost, @post) # => /blogposts/1
33
+ # url(:edit_blogpost, @post) # => /blogposts/1/edit
34
+ # url(:custom_new_blogpost, :action => 'alternate') # => /blogposts/new/alternate
35
+ #
36
+ # url(:comments, :blogpost => @post) # => /blogposts/1/comments
37
+ # url(:new_comment, :blogpost => @post) # => /blogposts/1/comments/new
38
+ # url(:comment, @comment) # => /blogposts/1/comments/1
39
+ # url(:edit_comment, @comment) # => /blogposts/1/comments/1/edit
40
+ # url(:custom_new_comment, :blogpost => @post)
41
+ #
42
+ def url(path, o={})
43
+ ::Merb::Router.generate(path,o)
44
+ end
45
+
46
+ protected
3
47
  NAME_REGEX = /Content-Disposition:.* name="?([^\";]*)"?/ni.freeze
4
48
  CONTENT_TYPE_REGEX = /Content-Type: (.*)\r\n/ni.freeze
5
49
  FILENAME_REGEX = /Content-Disposition:.* filename="?([^\";]*)"?/ni.freeze
6
50
  CRLF = "\r\n".freeze
7
51
  EOL = CRLF
8
- def parse_multipart(request,boundary)
52
+ def parse_multipart(request,boundary,env)
9
53
  boundary = "--#{boundary}"
10
- paramhsh = MerbHash.new
54
+ paramhsh = {}
11
55
  buf = ""
12
- content_length = @env['CONTENT_LENGTH'].to_i
56
+ content_length = env['CONTENT_LENGTH'].to_i
13
57
  input = request
14
58
  input.binmode if defined? input.binmode
15
59
  boundary_size = boundary.size + EOL.size
@@ -86,10 +130,10 @@ module Merb
86
130
  def normalize_params(parms, key, val)
87
131
  case key
88
132
  when /(.+)\[(.+)\]\[\]$/
89
- parms[$1] ||= MerbHash.new
133
+ parms[$1] ||= {}
90
134
  parms[$1] = normalize_params(parms[$1], "#{$2}[]", val)
91
135
  when /(.+)\[(.+)\]$/
92
- parms[$1] ||= MerbHash.new
136
+ parms[$1] ||= {}
93
137
  parms[$1] = normalize_params(parms[$1], $2, val)
94
138
  when /(.+)\[\]$/
95
139
  (parms[$1] ||= []) << val
@@ -99,9 +143,6 @@ module Merb
99
143
  parms
100
144
  end
101
145
 
102
- def url(path, o={})
103
- ::Merb::Router.generator.generate(path,o)
104
- end
105
146
 
106
147
  # parses a query string or the payload of a POST
107
148
  # request into the params hash. So for example:
@@ -109,11 +150,11 @@ module Merb
109
150
  # parses into:
110
151
  # {:bar => 'nik', :post => {:title => 'heya', :body => 'whatever'}}
111
152
  def query_parse(qs, d = '&;')
112
- m = proc {|_,o,n|o.u(n,&m)rescue([*o]<<n)}
113
- (qs||'').split(/[#{d}] */n).inject(MerbHash[]) { |h,p|
153
+ m = proc {|_,o,n|o.update(n,&m)rescue([*o]<<n)}
154
+ (qs||'').split(/[#{d}] */n).inject(Hash[]) { |h,p|
114
155
  k, v=unescape(p).split('=',2)
115
- h.u(k.split(/[\]\[]+/).reverse.
116
- inject(v) { |x,i| MerbHash[i,x] },&m)
156
+ h.update(k.split(/[\]\[]+/).reverse.
157
+ inject(v) { |x,i| Hash[i,x] },&m)
117
158
  }
118
159
  end
119
160
 
@@ -124,7 +165,7 @@ module Merb
124
165
  # render_chunked do
125
166
  # IO.popen("cat /tmp/test.log") do |io|
126
167
  # done = false
127
- # until done == true do
168
+ # until done
128
169
  # sleep 0.3
129
170
  # line = io.gets.chomp
130
171
  # if line == 'EOF'
@@ -146,6 +187,15 @@ module Merb
146
187
  }
147
188
  end
148
189
 
190
+ def render_deferred(&blk)
191
+ Proc.new {
192
+ result = blk.call
193
+ response.send_status(result.length)
194
+ response.send_header
195
+ response.write(result)
196
+ }
197
+ end
198
+
149
199
  # for use within a render_chunked response
150
200
  def send_chunk(data)
151
201
  response.write('%x' % data.size + "\r\n")
@@ -157,7 +207,7 @@ module Merb
157
207
  # be a fully qualified url to another site.
158
208
  def redirect(url)
159
209
  MERB_LOGGER.info("Redirecting to: #{url}")
160
- @status = 302
210
+ set_status(302)
161
211
  headers.merge!({'Location'=> url})
162
212
  return ''
163
213
  end
@@ -181,8 +231,8 @@ module Merb
181
231
  # stream_file( { :filename => file_name,
182
232
  # :type => content_type,
183
233
  # :content_length => content_length }) do
184
- # @response.send_status(opts[:content_length])
185
- # @response.send_header
234
+ # response.send_status(opts[:content_length])
235
+ # response.send_header
186
236
  # AWS::S3::S3Object.stream(user.folder_name + "-" + user_file.unique_id, bucket_name) do |chunk|
187
237
  # @response.write chunk
188
238
  # end
@@ -191,7 +241,7 @@ module Merb
191
241
  opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts))
192
242
  disposition = opts[:disposition].dup || 'attachment'
193
243
  disposition << %(; filename="#{opts[:filename]}")
194
- @response.headers.update(
244
+ response.headers.update(
195
245
  'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers
196
246
  'Content-Disposition' => disposition,
197
247
  'Content-Transfer-Encoding' => 'binary',
@@ -228,17 +278,6 @@ module Merb
228
278
  Digest::MD5.hexdigest("#{inspect}#{Time.now}#{rand}")
229
279
  end
230
280
 
231
- def rand_uuid
232
- "%04x%04x-%04x-%04x-%04x-%06x%06x" % [
233
- rand(0x0010000),
234
- rand(0x0010000),
235
- rand(0x0010000),
236
- rand(0x0010000),
237
- rand(0x0010000),
238
- rand(0x1000000),
239
- rand(0x1000000),
240
- ]
241
- end
242
281
 
243
282
  def escape_xml(obj)
244
283
  obj.to_s.gsub(/[&<>"']/) { |s| Merb::Const::ESCAPE_TABLE[s] }
@@ -41,14 +41,7 @@ module Merb
41
41
  def throw_content(name, content = nil, &block)
42
42
  eval "@_#{name}_content = (@_#{name}_content || '') + capture(&block)"
43
43
  end
44
-
45
- # catch_content catches the thrown content from another template
46
- # So when you throw_content(:foo) {...} you can catch_content :foo
47
- # in another view or the layout.
48
- def catch_content(name)
49
- instance_variable_get("@_#{name}_content")
50
- end
51
-
44
+
52
45
  private
53
46
  def capture_block(*args, &block)
54
47
  block.call(*args)
@@ -2,7 +2,7 @@ require 'date'
2
2
  require 'ostruct'
3
3
 
4
4
  class OpenStruct
5
- def temp hash
5
+ def temp(hash)
6
6
  OpenStruct.new(@table.merge(hash))
7
7
  end
8
8
  end
@@ -10,91 +10,329 @@ end
10
10
  module Merb
11
11
  module FormControls
12
12
  #
13
- # this is the main merb form control helper. It can build
14
- # either textfield, textarea, date selects, or time selects.
13
+ # This is the main merb form control helper.
14
+ # The types that can be rendered are
15
+ #
16
+ # text => A standard textfield
17
+ # textarea => A textarea
18
+ # password => A password field
19
+ # date => A select menu with day, month and year
20
+ # time => As date + hour, minute and second
21
+ # select => A select menu for a custom collection in Array or Hash
15
22
  #
16
- # <%= control_for @post, :title, :text, :id => 'foo', :size => 53 %>
17
- # <%= control_for @post, :intro, :textarea, :class => 'post_intro',
18
- # :rows => 10, :cols => 50 %>
19
- # <%= control_for @post, :created_at, :time %>
20
- # <%= control_for @post, :published_at, :date %>
21
23
  #
22
- # TODO : is this useful enough? Needs some love
23
- def control_for(obj, meth, type, opts={})
24
+ # HTML-formatting and some control-options can be given in +options+
25
+ #
26
+ # === Defaults
27
+ # id => obj.class (class_name_in_camel_case)
28
+ # name => obj.class[meth] (class_name_in_camel_case[control_method])
29
+ # value => The value of obj.meth
30
+ #
31
+ # ==== Options
32
+ # +HTML+, +DOM+ and +CSS+
33
+ # :class, :size, :rows, :cols, :name, ...
34
+ #
35
+ # +label+
36
+ # Setting this label will include a label tag pointing at the field that
37
+ # displays the value of :label in the options hash
38
+ #
39
+ #
40
+ # ==== Examples
41
+ # The bare minimum
42
+ # <%= control_for @post, :title, :text%>
43
+ # renders a textfield for @post.title
44
+ #
45
+ # <%= control_for @post, :content, :textarea %>
46
+ # renders a textarea for @post.content
47
+ #
48
+ # <%= control_for @post, :secret, :password %>
49
+ # renders a password-field for @post.secret
50
+ #
51
+ # Some HTML and CSS options
52
+ # <%= control_for @post, :title, :text,
53
+ # :id => 'foo', :size => 53 %>
54
+ #
55
+ # <%= control_for @post, :content, :textarea,
56
+ # :class => 'post_intro', :rows => 10, :cols => 50 %>
57
+ #
58
+ #
59
+ # === Time and date
60
+ #
61
+ # +monthnames+
62
+ # for time and date selects, shows monthnames as text.
63
+ # An array of monthnames can be supplied
64
+ # (defaults to Date::MONTHNAMES)
65
+ #
66
+ # +min_year+ and +max_year+
67
+ # for time and date selects, sets the first and last year
68
+ # (defaults to 1950 and 2050)
69
+ #
70
+ # ==== Examples
71
+ #
72
+ # Simple time and date
73
+ # <%= control_for @post, :created_at, :time %>
74
+ # <%= control_for @post, :published_at, :date %>
75
+ #
76
+ # In time and date controls, monthnames can be added
77
+ # <%= control_for @post, :created_at, :time, :monthnames => true %>
78
+ #
79
+ # You can also specify other month-names
80
+ # <%= control_for @post, :published_at, :date,
81
+ # :monthnames => Date::ABBR_MONTHNAMES %>
82
+ # or
83
+ # french_months = [nil] + %w(janvier février mars avril mai juin
84
+ # juillet août septembre octobre novembre décembre)
85
+ #
86
+ # <%= control_for @post, :published_at, :date,
87
+ # {:monthnames => french_months} %>
88
+ #
89
+ # In time and date controls, :min_year and :max_year can be specified.
90
+ # Defaults to 1950..2050
91
+ # <%= control_for @post, :created_at, :time,
92
+ # :min_year => 2000, :max_year => 2010 %>
93
+ #
94
+ # <%= control_for @post, :published_at, :date,
95
+ # :min_year => Date.today.year, :max_year => Date.today.year+2 %>
96
+ #
97
+ #
98
+ # === Select Tags
99
+ # The control_for( object, :method, :select, {:collection => collection} )
100
+ # creates a <select> tag that automatically selects the given object
101
+ #
102
+ # === Special Options
103
+ # +collection+
104
+ # The collection can be an array of objects, or a hash of arrays of objects.
105
+ # This is required if you want any options to be displayed
106
+ #
107
+ # +text_method+
108
+ # The method that will provide the text for the option to display to the user.
109
+ # By default it will use the control method
110
+ #
111
+ # +include_blank+
112
+ # Includes a blank option at the top of the list if set to true
113
+ #
114
+ # All options provided to the call to control_for are rendered as xml/html tag attributes
115
+ #
116
+ # ==== Examples
117
+ #
118
+ # Imagine cars, with a brand (BMW, Toyota, ...) a model
119
+ # (Z3, Carina, Prius) and some internal code.
120
+ #
121
+ # class Car
122
+ # ...
123
+ # attr_reader :brand, :model, :code
124
+ # end
125
+ #
126
+ # An array of objects
127
+ # @all_cars = [ car1, car2, car3 ]
128
+ #
129
+ # <%= control_for @car2, :code, :select,
130
+ # {:text_method => :model, :collection => @all_cars } %>
131
+ #
132
+ # <select name="car[code]" id="car_code">
133
+ # <option value="code_for_car_1">Z3</option>
134
+ # <option selected="selected" value="code_for_car_2">Carina</option>
135
+ # <option value="code_for_car_3">Prius</option>
136
+ # </select>
137
+ #
138
+ # The same array of cars but run through a group_by on :brand to give a hash
139
+ #
140
+ # @all_cars = @all_cars.group_by{|car| car.brand }
141
+ #
142
+ # { :bmw => [car1],
143
+ # :toyota => [car2, car3]
144
+ # }
145
+ #
146
+ # <%= control_for @car2, :code, :select,
147
+ # {:text_method => :model, :collection => @all_cars } %>
148
+ #
149
+ # <select name="car[code]" id="car_code">
150
+ # <optgroup label="BMW">
151
+ # <option value="code_for_car_1">Z3</option>
152
+ # </optgroup>
153
+ # <optgroup label="Toyota">
154
+ # <option selected="selected" value="code_for_car_2">Carina</option>
155
+ # <option value="code_for_car_3">Prius</option>
156
+ # </optgroup>
157
+ # </select>
158
+ #
159
+
160
+
161
+ def control_for( obj, meth, type, opts = {} )
24
162
  instance = obj
25
163
  obj = obj.class
26
- o = OpenStruct.new(:name => "#{obj.to_s.downcase}[#{meth}]",
27
- :value => (instance.send(meth) rescue nil),
28
- :title => (opts.has_key?(:title) ? opts.delete(:title) : nil),
29
- :html => opts)
30
- Control.send(type, o)
164
+ # FooBar => foo_bar
165
+ # Admin::FooBar => admin_foo_bar
166
+ obj_dom_id = Inflector.underscore(obj.to_s).gsub('/', '_')
167
+ default_opts = {
168
+ # These are in here to make sure that they are set. They can be overridden if the user wants to.
169
+ :id => "#{obj_dom_id}_#{meth}",
170
+ :name => "#{obj_dom_id}[#{meth}]",
171
+ :value => (instance.send(meth) rescue nil) || ""
172
+ }
173
+ o = OpenStruct.new(
174
+ :value => default_opts[:value], # Just for convenience
175
+ :label => ( opts.has_key?( :label ) ? opts.delete( :label ) : nil ),
176
+ :monthnames => (if opts.has_key?(:monthnames)
177
+ opts[:monthnames]==true ? Date::MONTHNAMES : opts.delete(:monthnames)
178
+ else
179
+ nil
180
+ end),
181
+ :meth => meth,
182
+ :obj => instance,
183
+ :min_year => (opts.has_key?(:min_year) ? opts.delete(:min_year) : 1950),
184
+ :max_year => (opts.has_key?(:max_year) ? opts.delete(:max_year) : 2050),
185
+ :html => default_opts.merge(opts))
186
+ Control.send(type, o)
31
187
  end
32
188
 
33
189
  module Control
34
190
 
35
- # This is ripped wholesale from Ramaze with some modifications to work
36
- # with AR objects instead of Og. Thanks again to Michael Fellinger
191
+ # This was ripped wholesale from Ramaze and has had some fairly major modifications to work
192
+ # with AR objects instead of Og. Thanks again to Michael Fellinger.
37
193
  class << self
38
194
 
39
195
  def number(o)
40
- o.value ||= 0
196
+ o.value = o.html[:value] = 0 if o.value == ""
41
197
  text(o)
42
198
  end
43
199
 
44
200
  def hidden(o)
45
- o.value ||= ""
46
- %{<input type="hidden" name="#{o.name}" value="#{o.value}" #{o.html ? o.html.map{|k,v| "#{k}=\"#{v}\""}.join(' ') : nil}/>}
201
+ input_tag( o.html.merge( :type => 'hidden' ) )
47
202
  end
48
203
 
49
204
  def text(o)
50
- o.value ||= ""
51
205
  tag = ''
52
- tag << "#{o.title}: " if o.title
53
- tag << %{<input type="text" name="#{o.name}" value="#{o.value}" #{o.html ? o.html.map{|k,v| "#{k}=\"#{v}\""}.join(' ') : nil}/>}
206
+ tag << label_for_object( o )
207
+ tag << input_tag( o.html.merge( :type => 'text' ) )
54
208
  end
209
+
210
+ def password(o)
211
+ o.html.delete( :value ) if o.html.has_key?( :value ) # The password field should not be filled in
212
+ tag = ''
213
+ tag << label_for_object( o )
214
+ tag << input_tag( o.html.merge( :type => 'password' ) )
215
+ end
55
216
 
56
217
  def textarea(o)
57
- o.value ||= ""
58
- %{<textarea name="#{o.name}" #{o.html ? o.html.map{|k,v| "#{k}=\"#{v}\""}.join(' ') : nil}>#{o.value}</textarea>}
218
+ tag = ''
219
+ tag << label_for_object( o )
220
+ tag << %{<textarea #{options_as_attributes( o.html ) }>#{o.value}</textarea>}
59
221
  end
60
222
 
61
223
  def date(o)
62
224
  o.value ||= Date.today
63
225
  selects = []
64
- selects << date_day(o.temp(:value => o.value.day))
226
+ selects << label_for_object( o )
227
+ selects << date_day(o.temp(:value => o.value.day))
65
228
  selects << date_month(o.temp(:value => o.value.month))
66
- selects << date_year(o.temp(:value => o.value.year))
229
+ selects << date_year(o.temp(:value => o.value.year))
67
230
  selects.join("\n")
68
231
  end
69
232
 
70
233
  def time(o)
71
234
  o.value ||= Time.now
72
235
  selects = []
73
- selects << date_day(o.temp(:value => o.value.day))
74
- selects << date_month(o.temp(:value => o.value.month))
75
- selects << date_year(o.temp(:value => o.value.year))
76
- selects << time_hour(o.temp(:value => o.value.hour))
236
+ selects << label_for_object( o )
237
+ selects << date_day(o.temp(:value => o.value.day))
238
+ selects << date_month(o.temp(:value => o.value.month))
239
+ selects << date_year(o.temp(:value => o.value.year))
240
+ selects << time_hour(o.temp(:value => o.value.hour))
77
241
  selects << time_minute(o.temp(:value => o.value.min))
78
242
  selects << time_second(o.temp(:value => o.value.sec))
79
243
  selects.join("\n")
80
244
  end
81
245
 
82
- def time_second(o) select(o.name+'[second]', (0...60),o.value) end
83
- def time_minute(o) select(o.name+'[minute]', (0...60),o.value) end
84
- def time_hour(o) select(o.name+'[hour]', (0...24),o.value) end
85
- def date_day(o) select(o.name+'[day]', (1..31),o.value) end
86
- def date_month(o) select(o.name+'[month]', (1..12),o.value) end
87
- def date_year(o) select(o.name+'[year]', (1950..2050),o.value) end
88
-
89
- def select(name, range, default)
246
+ def time_second(o) select_tag(o.html[:name] +'[second]', (0...60),o.value) end
247
+ def time_minute(o) select_tag(o.html[:name] +'[minute]', (0...60),o.value) end
248
+ def time_hour(o) select_tag(o.html[:name] +'[hour]', (0...24),o.value) end
249
+ def date_day(o) select_tag(o.html[:name] +'[day]', (1..31),o.value) end
250
+ def date_month(o) select_tag(o.html[:name] +'[month]', (1..12),o.value, ( o.monthnames.compact unless o.monthnames.nil? )) end
251
+ def date_year(o) select_tag(o.html[:name] +'[year]', (o.min_year..o.max_year),o.value) end
252
+
253
+ def select( o )
254
+ options = {}
255
+ [:collection, :text_method, :include_blank].each do |value|
256
+ options[value] = o.html.has_key?( value ) ? o.html.delete( value ) : nil
257
+ end
258
+ out = ""
259
+ out << label_for_object( o )
260
+ out << %{<select #{options_as_attributes( o.html )}>}
261
+ out << %{#{options_for_select( o.obj, o.meth, options )}}
262
+ out << %{</select>}
263
+ end
264
+
265
+ # Creates an input tag with the given +option+ hash as xml/html attributes
266
+ def input_tag( options )
267
+ %{<input #{options_as_attributes( options ) }/>}
268
+ end
269
+
270
+ # Creates an select tag that is not nesiscarily bound to an objects value
271
+ # === Options
272
+ # +name+ The name of the select tag
273
+ # +range+ A range or array that specifies the values of the options
274
+ # +default+ The default value. This will be selected if present
275
+ # +txt+ A parrallel array of text values
276
+ def select_tag(name, range, default, txt = nil)
90
277
  out = %{<select name="#{name}">\n}
91
- range.each do |i|
92
- out << %{ <option value="#{i}"#{' selected="selected"' if default == i}>#{i}</option>\n}
278
+ range.each_with_index do |value, index |
279
+ out << option_for_select( value, (txt ? txt[index] : value ), (default == value ) )
93
280
  end
94
281
  out << "</select>\n"
95
282
  end
283
+
284
+ protected
285
+
286
+ # Creates a label from the openstruct created in control_for
287
+ def label_for_object( o )
288
+ o.label.nil? ? "" : %{<label for="#{o.html[:id]}">#{o.label}</label>}
289
+ end
290
+
291
+ # Converts a hash to use as attributes in an xml/html tag
292
+ def options_as_attributes( options )
293
+ options ? options.map{ |k,v| "#{k}=\"#{v}\""}.join( ' ' ) : nil
294
+ end
295
+
296
+ # The gateway to creating options for a select box
297
+ def options_for_select( obj, value_method, options )
298
+ text_method = options[:text_method] || value_method
299
+ collection = options[:collection] || []
300
+
301
+ out = ""
302
+ out = "<option></option>" if options[:include_blank]
303
+ out << case collection
304
+ when Array
305
+ options_for_select_from_array( obj, collection, value_method, text_method )
306
+ when Hash
307
+ options_for_select_from_hash( obj, collection, value_method, text_method )
308
+ end
309
+ end
310
+
311
+ def options_for_select_from_array( selected_object, collection, value_method, text_method )
312
+ out = ""
313
+ collection.each do | element|
314
+ out << option_for_select( element.send( value_method ), element.send( text_method ), (selected_object == element) )
315
+ end
316
+ out
317
+ end
318
+
319
+ def options_for_select_from_hash( selected_object, collection, value_method, text_method )
320
+ out = ""
321
+ collection.keys.sort.each do |key|
322
+ out << %{<optgroup label="#{key.to_s.humanize.titleize}">}
323
+ out << options_for_select_from_array( selected_object, collection[key], value_method, text_method )
324
+ out << %{</optgroup>}
325
+ end
326
+ out
327
+ end
328
+
329
+ # Creates that actual option tag for any given value, text and selected (true/false) combination
330
+ def option_for_select( value, text, selected )
331
+ out = %{<option#{ selected ? " selected=\"selected\"" : nil } }
332
+ out << %{value="#{value}">#{text}</option>}
333
+ end
96
334
  end
97
335
 
98
336
  end # Control
99
337
  end # FormHelper
100
- end # Merb
338
+ end # Merb