merb 0.3.4 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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