merb 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README +22 -4
- data/Rakefile +15 -3
- data/TODO +2 -3
- data/bin/merb +61 -36
- data/examples/sample_app/dist/app/controllers/files.rb +31 -0
- data/examples/sample_app/dist/app/controllers/posts.rb +26 -2
- data/examples/sample_app/dist/app/controllers/test.rb +7 -1
- data/examples/sample_app/dist/app/views/files/progress.jerb +3 -0
- data/examples/sample_app/dist/app/views/files/start.herb +62 -0
- data/examples/sample_app/dist/app/views/files/upload.herb +6 -0
- data/examples/sample_app/dist/app/views/layout/{application.rhtml → application.herb} +2 -3
- data/examples/sample_app/dist/app/views/layout/{foo.rhtml → foo.herb} +0 -0
- data/examples/sample_app/dist/app/views/posts/{_comments.rhtml → _comments.herb} +0 -0
- data/examples/sample_app/dist/app/views/posts/comment.jerb +1 -0
- data/examples/sample_app/dist/app/views/posts/{list.rhtml → list.herb} +0 -0
- data/examples/sample_app/dist/app/views/posts/{new.rhtml → new.herb} +0 -0
- data/examples/sample_app/dist/app/views/posts/{show.rhtml → show.herb} +0 -0
- data/examples/sample_app/dist/app/views/posts/xml_test.xerb +3 -0
- data/examples/sample_app/dist/app/views/test/{foo.rhtml → foo.herb} +0 -0
- data/examples/sample_app/dist/app/views/test/{hello.rhtml → hello.herb} +0 -0
- data/examples/sample_app/dist/app/views/test/json.jerb +1 -0
- data/examples/sample_app/dist/conf/merb.yml +11 -0
- data/examples/sample_app/dist/conf/merb_init.rb +1 -1
- data/examples/sample_app/dist/conf/mup.conf +11 -0
- data/examples/sample_app/dist/public/javascripts/mup.js +113 -0
- data/examples/sample_app/script/merb_stop +7 -3
- data/examples/sample_app/script/startdrb +8 -0
- data/lib/merb.rb +37 -2
- data/lib/merb/merb_class_extensions.rb +21 -22
- data/lib/merb/merb_controller.rb +101 -33
- data/lib/merb/merb_handler.rb +26 -25
- data/lib/merb/merb_router.rb +1 -1
- data/lib/merb/merb_utils.rb +35 -37
- data/lib/merb/mixins/basic_authentication_mixin.rb +39 -0
- data/lib/merb/mixins/controller_mixin.rb +119 -115
- data/lib/merb/mixins/javascript_mixin.rb +63 -0
- data/lib/merb/mixins/render_mixin.rb +85 -69
- data/lib/merb/mixins/responder_mixin.rb +38 -0
- data/lib/merb/session/merb_drb_server.rb +107 -0
- data/lib/merb/session/merb_drb_session.rb +71 -0
- data/lib/merb/session/merb_session.rb +1 -0
- data/lib/merb/vendor/paginator/README.txt +84 -0
- data/lib/merb/vendor/paginator/paginator.rb +121 -0
- data/lib/mutex_hotfix.rb +34 -0
- metadata +41 -63
- data/doc/rdoc/classes/ControllerMixin.html +0 -676
- data/doc/rdoc/classes/Hash.html +0 -148
- data/doc/rdoc/classes/Merb.html +0 -140
- data/doc/rdoc/classes/Merb/Controller.html +0 -338
- data/doc/rdoc/classes/Merb/RouteMatcher.html +0 -388
- data/doc/rdoc/classes/Merb/Server.html +0 -148
- data/doc/rdoc/classes/Merb/Session.html +0 -201
- data/doc/rdoc/classes/Merb/SessionMixin.html +0 -199
- data/doc/rdoc/classes/MerbControllerError.html +0 -111
- data/doc/rdoc/classes/MerbHandler.html +0 -430
- data/doc/rdoc/classes/MerbHash.html +0 -469
- data/doc/rdoc/classes/MerbHash/Mutex.html +0 -198
- data/doc/rdoc/classes/Noroutefound.html +0 -153
- data/doc/rdoc/classes/Object.html +0 -149
- data/doc/rdoc/classes/RenderMixin.html +0 -362
- data/doc/rdoc/classes/String.html +0 -212
- data/doc/rdoc/classes/Symbol.html +0 -179
- data/doc/rdoc/created.rid +0 -1
- data/doc/rdoc/files/LICENSE.html +0 -129
- data/doc/rdoc/files/README.html +0 -417
- data/doc/rdoc/files/TODO.html +0 -151
- data/doc/rdoc/files/lib/merb/merb_class_extensions_rb.html +0 -101
- data/doc/rdoc/files/lib/merb/merb_controller_rb.html +0 -101
- data/doc/rdoc/files/lib/merb/merb_handler_rb.html +0 -101
- data/doc/rdoc/files/lib/merb/merb_router_rb.html +0 -101
- data/doc/rdoc/files/lib/merb/merb_utils_rb.html +0 -108
- data/doc/rdoc/files/lib/merb/mixins/controller_mixin_rb.html +0 -101
- data/doc/rdoc/files/lib/merb/mixins/render_mixin_rb.html +0 -101
- data/doc/rdoc/files/lib/merb/session/merb_session_rb.html +0 -101
- data/doc/rdoc/files/lib/merb_rb.html +0 -140
- data/doc/rdoc/files/lib/merb_tasks_rb.html +0 -101
- data/doc/rdoc/fr_class_index.html +0 -43
- data/doc/rdoc/fr_file_index.html +0 -40
- data/doc/rdoc/fr_method_index.html +0 -104
- data/doc/rdoc/index.html +0 -24
- data/doc/rdoc/rdoc-style.css +0 -208
- data/examples/sample_app/dist/app/controllers/upload.rb +0 -29
- data/examples/sample_app/dist/app/views/posts/comment.merbjs +0 -1
- data/examples/sample_app/dist/app/views/upload/start.rhtml +0 -15
- data/examples/sample_app/dist/app/views/upload/upload.rhtml +0 -4
- data/examples/sample_app/dist/public/files/README +0 -35
- data/examples/sample_app/dist/public/files/setup.rb +0 -1346
- data/examples/sample_app/log/merb.log +0 -778
@@ -0,0 +1,63 @@
|
|
1
|
+
module Merb
|
2
|
+
module JavascriptMixin
|
3
|
+
|
4
|
+
# escape text for javascript.
|
5
|
+
def escape_js(javascript)
|
6
|
+
(javascript || '').gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
|
7
|
+
end
|
8
|
+
|
9
|
+
def link_to_function(name, function)
|
10
|
+
%{<a href="#" onclick="#{function}; return false;">#{name}</a>}
|
11
|
+
end
|
12
|
+
|
13
|
+
def js(data)
|
14
|
+
if data.respond_to? :to_json
|
15
|
+
data.to_json
|
16
|
+
else
|
17
|
+
data.inspect.to_json
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def require_js(*scripts)
|
22
|
+
return nil if scripts.empty?
|
23
|
+
scripts.inject('') do |memo,script|
|
24
|
+
script = script.to_s
|
25
|
+
memo << %Q|<script src="/javascripts/#{script=~/\.js$/ ? script : script+'.js' }" type="text/javascript">//</script>\n|
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def require_css(*scripts)
|
30
|
+
return nil if scripts.empty?
|
31
|
+
scripts.inject('') do |memo,script|
|
32
|
+
script = script.to_s
|
33
|
+
memo << %Q|<link href="/stylesheets/#{script=~/\.css$/ ? script : script+'.css' }" media="all" rel="Stylesheet" type="text/css"/>\n|
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def js_hash(options)
|
38
|
+
'{' + options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
|
39
|
+
end
|
40
|
+
|
41
|
+
def insert_html(id, html, options = {})
|
42
|
+
position = options.fetch(:where, :before)
|
43
|
+
"new Insertion.#{position.to_s.camel_case}('#{id}', '#{escape_js html}');"
|
44
|
+
end
|
45
|
+
|
46
|
+
def replace_html(id, html, options = {})
|
47
|
+
"Element.update('#{id}', '#{escape_js html}');"
|
48
|
+
end
|
49
|
+
|
50
|
+
def hide(id)
|
51
|
+
"$('#{id}').style.display = 'none';"
|
52
|
+
end
|
53
|
+
|
54
|
+
def show(id)
|
55
|
+
"$('#{id}').style.display = 'block';"
|
56
|
+
end
|
57
|
+
|
58
|
+
def toggle(id)
|
59
|
+
"Element.toggle('#{id}');"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -1,75 +1,91 @@
|
|
1
|
-
module
|
1
|
+
module Merb
|
2
2
|
|
3
|
-
|
4
|
-
def template_dir(loc)
|
5
|
-
File.expand_path(Merb::Server.config[:merb_root] + "/dist/app/views/#{loc}")
|
6
|
-
end
|
7
|
-
|
8
|
-
# returns the current method name. Used for
|
9
|
-
# auto discovery of which template to render
|
10
|
-
# based on the action name.
|
11
|
-
def current_method_name(depth=0)
|
12
|
-
caller[depth] =~ /`(.*)'$/; $1
|
13
|
-
end
|
14
|
-
|
15
|
-
# does a render with no layout. Also sets the
|
16
|
-
# content type header to text/javascript and
|
17
|
-
# escapes the template for javascript eval on
|
18
|
-
# the client
|
19
|
-
def render_js(template=current_method_name(1), b=binding)
|
20
|
-
headers['Content-Type'] = "text/javascript"
|
21
|
-
template = Erubis::Eruby.new(IO.read( template_dir(self.class.name.snake_case) + "/#{template}.merbjs" ))
|
22
|
-
template.result(b)
|
23
|
-
end
|
24
|
-
|
25
|
-
# set the @layout. Use this right before a render to
|
26
|
-
# set the name of the layout to use minus the .rhtml
|
27
|
-
def layout(l)
|
28
|
-
@layout = l
|
29
|
-
end
|
3
|
+
module RenderMixin
|
30
4
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
name
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
5
|
+
# shortcut to a template path based on name.
|
6
|
+
def template_dir(loc)
|
7
|
+
File.expand_path(Merb::Server.config[:merb_root] + "/dist/app/views/#{loc}")
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns the current method name. Used for
|
11
|
+
# auto discovery of which template to render
|
12
|
+
# based on the action name.
|
13
|
+
def current_method_name(depth=0)
|
14
|
+
caller[depth] =~ /`(.*)'$/; $1
|
15
|
+
end
|
16
|
+
|
17
|
+
def template_extension_for(ext)
|
18
|
+
Merb::Server.config[:template_ext][ext]
|
19
|
+
end
|
20
|
+
|
21
|
+
# does a render with no layout. Also sets the
|
22
|
+
# content type header to text/javascript
|
23
|
+
def render_js(template=current_method_name(1), b=binding)
|
24
|
+
headers['Content-Type'] = "text/javascript"
|
25
|
+
template = Erubis::Eruby.new(IO.read( template_dir(self.class.name.snake_case) + "/#{template}.#{template_extension_for(:js)}" ))
|
26
|
+
template.result(b)
|
27
|
+
end
|
28
|
+
|
29
|
+
# set the @layout. Use this right before a render to
|
30
|
+
# set the name of the layout to use minus the .rhtml
|
31
|
+
def layout(l)
|
32
|
+
@layout = l
|
33
|
+
end
|
34
|
+
|
35
|
+
# renders nothing but sets the status
|
36
|
+
def render_nothing(status=200)
|
37
|
+
@status = status
|
38
|
+
return "\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
# renders the action without wrapping it in a layout.
|
42
|
+
def render_no_layout(template=current_method_name(1), b=binding)
|
43
|
+
template = Erubis::Eruby.new( IO.read( template_dir(self.class.name.snake_case) + "/#{template}.#{template_extension_for(:html)}" ) )
|
44
|
+
template.result(b)
|
45
|
+
end
|
46
|
+
|
47
|
+
def partial(template)
|
48
|
+
template = Erubis::Eruby.new( IO.read( template_dir(self.class.name.snake_case) + "/_#{template}.#{template_extension_for(:html)}" ) )
|
49
|
+
template.result(binding)
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_xml(template=current_method_name(1))
|
53
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
54
|
+
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
55
|
+
eval IO.read( template_dir(self.class.name.snake_case) + "/#{template}.#{template_extension_for(:xml)}" )
|
56
|
+
@headers['Content-Type'] = 'application/xml'
|
57
|
+
@headers['Encoding'] = 'UTF-8'
|
58
|
+
xml.target!
|
59
|
+
end
|
60
|
+
|
61
|
+
# renders a template based on the current action name
|
62
|
+
# you can pass the name of a template if you want to
|
63
|
+
# render a template with a different name then then
|
64
|
+
# current action name. Wraps the rendered template in
|
65
|
+
# the layout. Uses layout/application.rhtml unless
|
66
|
+
# there is a layout named after the current controller
|
67
|
+
# or @layout has been set to another value.
|
68
|
+
def render(template=current_method_name(1), b=binding)
|
69
|
+
tmpl_ext = template_extension_for(:html)
|
70
|
+
MERB_LOGGER.info("Rendering template: #{template_dir(template)}..#{tmpl_ext}")
|
71
|
+
name = self.class.name.snake_case
|
72
|
+
template = Erubis::Eruby.new( IO.read( template_dir(name) + "/#{template}.#{tmpl_ext}" ) )
|
73
|
+
layout_content = template.result(b)
|
74
|
+
return layout_content if (@layout.to_s == 'none')
|
75
|
+
if ['application', name].include?(@layout.to_s)
|
76
|
+
if File.exist?(template_dir("layout/#{name}.#{tmpl_ext}"))
|
77
|
+
layout = name
|
78
|
+
else
|
79
|
+
layout = 'application'
|
80
|
+
end
|
63
81
|
else
|
64
|
-
layout =
|
82
|
+
layout = @layout.to_s
|
65
83
|
end
|
66
|
-
|
67
|
-
|
84
|
+
MERB_LOGGER.info("With Layout: #{template_dir('layout')}/#{layout}.#{tmpl_ext}")
|
85
|
+
@layout_content = layout_content
|
86
|
+
layout_tmpl = Erubis::Eruby.new( IO.read( "#{template_dir('layout')}/#{layout}.#{tmpl_ext}" ) )
|
87
|
+
layout_tmpl.result(b)
|
68
88
|
end
|
69
|
-
|
70
|
-
|
71
|
-
layout_tmpl = Erubis::Eruby.new( IO.read( template_dir('layout') + "/#{layout}.rhtml" ) )
|
72
|
-
layout_tmpl.result(b)
|
73
|
-
end
|
74
|
-
|
89
|
+
|
90
|
+
end
|
75
91
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Merb
|
2
|
+
# Thanks to Chris Wanstrath
|
3
|
+
# use this in your controllers to switch output based on
|
4
|
+
# the HTTP_ACCEPT header. like so:
|
5
|
+
# respond_to do |type|
|
6
|
+
# type.js { render_js }
|
7
|
+
# type.html { render }
|
8
|
+
# type.xml { @foo.to_xml }
|
9
|
+
# type.yaml { @foo.to_yaml }
|
10
|
+
# end
|
11
|
+
|
12
|
+
module ResponderMixin
|
13
|
+
def respond_to
|
14
|
+
yield response = Response.new(@env['HTTP_ACCEPT'])
|
15
|
+
@headers['Content-Type'] = response.content_type
|
16
|
+
response.body
|
17
|
+
end
|
18
|
+
|
19
|
+
class Response
|
20
|
+
attr_reader :body, :content_type
|
21
|
+
def initialize(accept) @accept = accept end
|
22
|
+
|
23
|
+
TYPES = {
|
24
|
+
:yaml => %w[application/yaml text/yaml],
|
25
|
+
:text => %w[text/plain],
|
26
|
+
:html => %w[text/html */* application/html],
|
27
|
+
:xml => %w[application/xml]
|
28
|
+
}
|
29
|
+
|
30
|
+
def method_missing(method, *args)
|
31
|
+
if TYPES[method] && @accept =~ Regexp.union(*TYPES[method])
|
32
|
+
@content_type = TYPES[method].first
|
33
|
+
@body = yield if block_given?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Merb
|
5
|
+
|
6
|
+
class DRbSession
|
7
|
+
|
8
|
+
include DRbUndumped
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def setup(opts={})
|
13
|
+
@opts = opts
|
14
|
+
@sessions = Hash.new
|
15
|
+
@timestamps = Hash.new
|
16
|
+
@mutex = Mutex.new
|
17
|
+
@session_ttl = opts.fetch(:session_ttl, 15*60) # default 15 minutes
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def create(opts={})
|
22
|
+
self[opts[:sess_id]] = opts[:data]
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
@mutex.synchronize {
|
27
|
+
@timestamps[key] = Time.now
|
28
|
+
@sessions[key]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def []=(key, val)
|
33
|
+
@mutex.synchronize {
|
34
|
+
@timestamps[key] = Time.now
|
35
|
+
@sessions[key] = val
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete(key)
|
40
|
+
@mutex.synchronize {
|
41
|
+
@sessions.delete(key)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def reap_old_sessions
|
46
|
+
@timestamps.each do |key,stamp|
|
47
|
+
if stamp + @session_ttl < Time.now
|
48
|
+
delete(key)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
GC.start
|
52
|
+
end
|
53
|
+
|
54
|
+
def sessions
|
55
|
+
@sessions
|
56
|
+
end
|
57
|
+
|
58
|
+
end # end singleton class
|
59
|
+
|
60
|
+
end # end DRbSession
|
61
|
+
|
62
|
+
# Keeps track of the status of all currently processing uploads
|
63
|
+
class UploadProgress
|
64
|
+
attr_accessor :debug
|
65
|
+
def initialize
|
66
|
+
@guard = Mutex.new
|
67
|
+
@counters = {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def check(upid)
|
71
|
+
@counters[upid].last rescue nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def last_checked(upid)
|
75
|
+
@counters[upid].first rescue nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_checked_time(upid)
|
79
|
+
@guard.synchronize { @counters[upid][0] = Time.now }
|
80
|
+
end
|
81
|
+
|
82
|
+
def add(upid, size)
|
83
|
+
@guard.synchronize do
|
84
|
+
@counters[upid] = [Time.now, {:size => size, :received => 0}]
|
85
|
+
puts "#{upid}: Added" if @debug
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def mark(upid, len)
|
90
|
+
return unless status = check(upid)
|
91
|
+
puts "#{upid}: Marking" if @debug
|
92
|
+
@guard.synchronize { status[:received] = status[:size] - len }
|
93
|
+
end
|
94
|
+
|
95
|
+
def finish(upid)
|
96
|
+
@guard.synchronize do
|
97
|
+
puts "#{upid}: Finished" if @debug
|
98
|
+
@counters.delete(upid)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def list
|
103
|
+
@counters.keys.sort
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Merb
|
2
|
+
|
3
|
+
module SessionMixin
|
4
|
+
|
5
|
+
def setup_session
|
6
|
+
MERB_LOGGER.info("Setting up session")
|
7
|
+
@session = Merb::Session.persist(cookies)
|
8
|
+
@fingerprint_before = Marshal.dump(@session).hash
|
9
|
+
end
|
10
|
+
|
11
|
+
def finalize_session
|
12
|
+
MERB_LOGGER.info("Finalize session")
|
13
|
+
unless Marshal.dump(@session).hash == @fingerprint_before
|
14
|
+
@session.save
|
15
|
+
end
|
16
|
+
@headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{escape(v)}; path=/" if v != @k[k] } - [nil]
|
17
|
+
end
|
18
|
+
|
19
|
+
# accessor for @session. Please use session and
|
20
|
+
# never @session directly.
|
21
|
+
def session
|
22
|
+
@session
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class Session
|
28
|
+
|
29
|
+
attr_reader :sess_id
|
30
|
+
|
31
|
+
def []=(k, v) # :nodoc:
|
32
|
+
(@data||={})[k] = v rescue nil
|
33
|
+
end
|
34
|
+
def [](k) # :nodoc:
|
35
|
+
@data[k] rescue nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(sess_id,data={})
|
39
|
+
@data = data || {}
|
40
|
+
@sess_id = sess_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def save
|
44
|
+
Merb::DRbSession[@sess_id] = @data
|
45
|
+
end
|
46
|
+
|
47
|
+
RAND_CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z']
|
48
|
+
|
49
|
+
# Generates a new session ID and creates a row for the new session in the database.
|
50
|
+
def self.generate(cookies)
|
51
|
+
rand_max = RAND_CHARS.size
|
52
|
+
sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] }
|
53
|
+
sess = Merb::DRbSession.create(:sess_id => sid, :data => {})
|
54
|
+
cookies[:sess_id] = sid
|
55
|
+
new(sid, sess)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Gets the existing session based on the <tt>camping_sid</tt> available in cookies.
|
59
|
+
# If none is found, generates a new session.
|
60
|
+
def self.persist cookies
|
61
|
+
if cookies[:sess_id]
|
62
|
+
session = new(cookies[:sess_id], Merb::DRbSession[cookies[:sess_id]])
|
63
|
+
end
|
64
|
+
unless session
|
65
|
+
session = Merb::Session.generate(cookies)
|
66
|
+
end
|
67
|
+
session
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|