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.
- data/README +206 -197
- data/Rakefile +12 -21
- data/bin/merb +1 -1
- data/examples/skeleton/Rakefile +6 -20
- data/examples/skeleton/dist/app/mailers/layout/application.erb +1 -0
- data/examples/skeleton/dist/conf/database.yml +23 -0
- data/examples/skeleton/dist/conf/environments/development.rb +1 -0
- data/examples/skeleton/dist/conf/environments/production.rb +1 -0
- data/examples/skeleton/dist/conf/environments/test.rb +1 -0
- data/examples/skeleton/dist/conf/merb.yml +32 -28
- data/examples/skeleton/dist/conf/merb_init.rb +16 -13
- data/examples/skeleton/dist/conf/router.rb +9 -9
- data/examples/skeleton/dist/schema/migrations/001_add_sessions_table.rb +2 -2
- data/lib/merb.rb +23 -18
- data/lib/merb/caching/fragment_cache.rb +3 -7
- data/lib/merb/caching/store/memcache.rb +20 -0
- data/lib/merb/core_ext/merb_array.rb +0 -0
- data/lib/merb/core_ext/merb_class.rb +44 -4
- data/lib/merb/core_ext/merb_enumerable.rb +43 -1
- data/lib/merb/core_ext/merb_hash.rb +200 -122
- data/lib/merb/core_ext/merb_kernel.rb +2 -0
- data/lib/merb/core_ext/merb_module.rb +41 -0
- data/lib/merb/core_ext/merb_numeric.rb +57 -5
- data/lib/merb/core_ext/merb_object.rb +172 -6
- data/lib/merb/generators/merb_app/merb_app.rb +15 -9
- data/lib/merb/merb_abstract_controller.rb +193 -0
- data/lib/merb/merb_constants.rb +26 -1
- data/lib/merb/merb_controller.rb +143 -234
- data/lib/merb/merb_dispatcher.rb +28 -20
- data/lib/merb/merb_drb_server.rb +2 -3
- data/lib/merb/merb_exceptions.rb +194 -49
- data/lib/merb/merb_handler.rb +34 -26
- data/lib/merb/merb_mail_controller.rb +200 -0
- data/lib/merb/merb_mailer.rb +33 -13
- data/lib/merb/merb_part_controller.rb +42 -0
- data/lib/merb/merb_plugins.rb +293 -0
- data/lib/merb/merb_request.rb +6 -4
- data/lib/merb/merb_router.rb +99 -65
- data/lib/merb/merb_server.rb +65 -21
- data/lib/merb/merb_upload_handler.rb +2 -1
- data/lib/merb/merb_view_context.rb +36 -15
- data/lib/merb/mixins/basic_authentication_mixin.rb +5 -5
- data/lib/merb/mixins/controller_mixin.rb +67 -28
- data/lib/merb/mixins/erubis_capture_mixin.rb +1 -8
- data/lib/merb/mixins/form_control_mixin.rb +280 -42
- data/lib/merb/mixins/render_mixin.rb +127 -45
- data/lib/merb/mixins/responder_mixin.rb +5 -7
- data/lib/merb/mixins/view_context_mixin.rb +260 -94
- data/lib/merb/session.rb +23 -0
- data/lib/merb/session/merb_ar_session.rb +28 -16
- data/lib/merb/session/merb_mem_cache_session.rb +108 -0
- data/lib/merb/session/merb_memory_session.rb +65 -20
- data/lib/merb/template/erubis.rb +22 -13
- data/lib/merb/template/haml.rb +5 -16
- data/lib/merb/template/markaby.rb +5 -3
- data/lib/merb/template/xml_builder.rb +17 -5
- data/lib/merb/test/merb_fake_request.rb +63 -0
- data/lib/merb/test/merb_multipart.rb +58 -0
- data/lib/tasks/db.rake +2 -0
- data/lib/tasks/merb.rake +20 -8
- metadata +24 -25
- 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
|
-
|
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
|
-
@
|
9
|
+
@controller
|
10
|
+
@_body
|
10
11
|
@_fingerprint_before
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@request
|
17
|
-
@status
|
12
|
+
@_session
|
13
|
+
@_headers
|
14
|
+
@_cookies
|
15
|
+
@_request
|
16
|
+
@_status
|
18
17
|
@_view_context_cache
|
19
|
-
@
|
20
|
-
@
|
18
|
+
@_response
|
19
|
+
@_params]
|
21
20
|
|
22
21
|
module GlobalHelper
|
23
22
|
end
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
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|
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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 =
|
54
|
+
paramhsh = {}
|
11
55
|
buf = ""
|
12
|
-
content_length =
|
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] ||=
|
133
|
+
parms[$1] ||= {}
|
90
134
|
parms[$1] = normalize_params(parms[$1], "#{$2}[]", val)
|
91
135
|
when /(.+)\[(.+)\]$/
|
92
|
-
parms[$1] ||=
|
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.
|
113
|
-
(qs||'').split(/[#{d}] */n).inject(
|
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.
|
116
|
-
inject(v) { |x,i|
|
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
|
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
|
-
|
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
|
-
#
|
185
|
-
#
|
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
|
-
|
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
|
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
|
-
#
|
14
|
-
#
|
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
|
-
#
|
23
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
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
|
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.
|
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 <<
|
53
|
-
tag <<
|
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
|
-
|
58
|
-
|
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 <<
|
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
|
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 <<
|
74
|
-
selects <<
|
75
|
-
selects <<
|
76
|
-
selects <<
|
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)
|
83
|
-
def time_minute(o)
|
84
|
-
def time_hour(o)
|
85
|
-
def date_day(o)
|
86
|
-
def date_month(o)
|
87
|
-
def date_year(o)
|
88
|
-
|
89
|
-
def select(
|
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.
|
92
|
-
out <<
|
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
|