jets 1.9.32 → 2.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/jets/application/defaults.rb +6 -0
- data/lib/jets/cli.rb +9 -0
- data/lib/jets/commands/help/generate.md +25 -19
- data/lib/jets/commands/main.rb +3 -3
- data/lib/jets/commands/rake_tasks.rb +3 -0
- data/lib/jets/commands/templates/skeleton/app/views/layouts/application.html.erb.tt +1 -0
- data/lib/jets/commands/templates/skeleton/config/application.rb.tt +4 -0
- data/lib/jets/commands/templates/webpacker/app/javascript/src/jets/crud.js +3 -0
- data/lib/jets/commands/upgrade.rb +42 -109
- data/lib/jets/commands/upgrade/version1.rb +136 -0
- data/lib/jets/controller/base.rb +16 -0
- data/lib/jets/controller/error.rb +4 -0
- data/lib/jets/controller/error/invalid_authenticity_token.rb +6 -0
- data/lib/jets/controller/forgery_protection.rb +43 -0
- data/lib/jets/controller/middleware/local.rb +3 -3
- data/lib/jets/controller/middleware/local/route_matcher.rb +1 -1
- data/lib/jets/controller/middleware/main.rb +7 -1
- data/lib/jets/controller/rack/adapter.rb +1 -1
- data/lib/jets/controller/rack/env.rb +1 -1
- data/lib/jets/controller/rendering/rack_renderer.rb +44 -37
- data/lib/jets/controller/stage.rb +2 -1
- data/lib/jets/generator.rb +72 -8
- data/lib/jets/generator/templates/active_job/job/templates/application_job.rb.tt +6 -0
- data/lib/jets/generator/templates/active_job/job/templates/job.rb.tt +8 -0
- data/lib/jets/generator/templates/erb/scaffold/_form.html.erb +11 -16
- data/lib/jets/generator/templates/erb/scaffold/edit.html.erb +2 -2
- data/lib/jets/generator/templates/erb/scaffold/index.html.erb +5 -5
- data/lib/jets/generator/templates/erb/scaffold/new.html.erb +1 -1
- data/lib/jets/generator/templates/erb/scaffold/show.html.erb +3 -3
- data/lib/jets/generator/templates/rails/scaffold_controller/controller.rb +5 -5
- data/lib/jets/internal/app/controllers/jets/rack_controller.rb +1 -0
- data/lib/jets/overrides/rails.rb +2 -1
- data/lib/jets/overrides/rails/action_controller.rb +12 -0
- data/lib/jets/overrides/rails/url_helper.rb +66 -5
- data/lib/jets/resource/api_gateway/rest_api/routes/change/base.rb +1 -1
- data/lib/jets/resource/api_gateway/rest_api/routes/collision.rb +1 -1
- data/lib/jets/router.rb +32 -46
- data/lib/jets/router/dsl.rb +136 -0
- data/lib/jets/router/error.rb +4 -0
- data/lib/jets/router/helpers.rb +4 -0
- data/lib/jets/router/helpers/core_helper.rb +17 -0
- data/lib/jets/router/helpers/named_routes_helper.rb +8 -0
- data/lib/jets/router/method_creator.rb +54 -0
- data/lib/jets/router/method_creator/code.rb +98 -0
- data/lib/jets/router/method_creator/edit.rb +7 -0
- data/lib/jets/router/method_creator/generic.rb +11 -0
- data/lib/jets/router/method_creator/index.rb +42 -0
- data/lib/jets/router/method_creator/new.rb +7 -0
- data/lib/jets/router/method_creator/root.rb +15 -0
- data/lib/jets/router/method_creator/show.rb +7 -0
- data/lib/jets/router/resources/base.rb +7 -0
- data/lib/jets/router/resources/filter.rb +15 -0
- data/lib/jets/router/resources/options.rb +13 -0
- data/lib/jets/router/route.rb +226 -0
- data/lib/jets/router/scope.rb +65 -4
- data/lib/jets/router/util.rb +38 -0
- data/lib/jets/turbo/project/config/application.rb +1 -0
- data/lib/jets/version.rb +1 -1
- metadata +26 -2
- data/lib/jets/route.rb +0 -166
data/lib/jets/controller/base.rb
CHANGED
@@ -10,6 +10,8 @@ class Jets::Controller
|
|
10
10
|
include Params
|
11
11
|
include Rendering
|
12
12
|
include ActiveSupport::Rescuable
|
13
|
+
include Jets::Router::Helpers
|
14
|
+
include ForgeryProtection
|
13
15
|
|
14
16
|
delegate :headers, to: :request
|
15
17
|
delegate :set_header, to: :response
|
@@ -78,6 +80,20 @@ class Jets::Controller
|
|
78
80
|
JSON.dump(data)
|
79
81
|
end
|
80
82
|
|
83
|
+
def controller_paths
|
84
|
+
paths = []
|
85
|
+
klass = self.class
|
86
|
+
while klass != Jets::Controller::Base
|
87
|
+
paths << klass.controller_path
|
88
|
+
klass = klass.superclass
|
89
|
+
end
|
90
|
+
paths
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.controller_path
|
94
|
+
name.sub(/Controller$/, "".freeze).underscore
|
95
|
+
end
|
96
|
+
|
81
97
|
def self.process(event, context={}, meth)
|
82
98
|
controller = new(event, context, meth)
|
83
99
|
# Using send because process! is private method in Jets::RackController so
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Jets::Controller
|
2
|
+
module ForgeryProtection
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
config = Jets.config
|
7
|
+
default_protect_from_forgery = config.dig(:controllers, :default_protect_from_forgery)
|
8
|
+
if default_protect_from_forgery
|
9
|
+
protect_from_forgery
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def protect_from_forgery(options = {})
|
15
|
+
before_action :verify_authenticity_token, options
|
16
|
+
end
|
17
|
+
|
18
|
+
def skip_forgery_protection
|
19
|
+
skip_before_action :verify_authenticity_token
|
20
|
+
end
|
21
|
+
|
22
|
+
def forgery_protection_enabled?
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# before_actions [[:verify_authenticity_token, {}], [:set_post, {:only=>[:show, :edit, :update, :delete]}
|
26
|
+
#
|
27
|
+
before_actions.map { |a| a[0] }.include?(:verify_authenticity_token)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Instance methods
|
32
|
+
def verify_authenticity_token
|
33
|
+
return true if ENV['TEST'] || request.get? || request.head?
|
34
|
+
|
35
|
+
token = session[:authenticity_token]
|
36
|
+
verified = !token.nil? && (token == params[:authenticity_token] || token == request.headers["x-csrf-token"])
|
37
|
+
|
38
|
+
unless verified
|
39
|
+
raise Error::InvalidAuthenticityToken
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'kramdown'
|
2
2
|
|
3
|
-
# Handles
|
3
|
+
# Handles mimicing of API Gateway to Lambda function call locally
|
4
4
|
module Jets::Controller::Middleware
|
5
5
|
class Local
|
6
6
|
extend Memoist
|
@@ -35,7 +35,7 @@ module Jets::Controller::Middleware
|
|
35
35
|
# This can only really get called with the local server.
|
36
36
|
run_polymophic_function
|
37
37
|
else # Normal Jets request
|
38
|
-
|
38
|
+
mimic_aws_lambda!(env, mimic.vars) unless on_aws?(env)
|
39
39
|
@app.call(env)
|
40
40
|
end
|
41
41
|
end
|
@@ -60,7 +60,7 @@ module Jets::Controller::Middleware
|
|
60
60
|
end
|
61
61
|
|
62
62
|
# Modifies env the in the same way real call from AWS lambda would modify env
|
63
|
-
def
|
63
|
+
def mimic_aws_lambda!(env, vars)
|
64
64
|
env.merge!(vars)
|
65
65
|
env
|
66
66
|
end
|
@@ -89,7 +89,7 @@ class Jets::Controller::Middleware::Local
|
|
89
89
|
# posts/:id/edit => posts\/(.*)\/edit
|
90
90
|
|
91
91
|
regexp_string = route_path.split('/').map do |s|
|
92
|
-
s.include?(':') ? Jets::Route::CAPTURE_REGEX : s
|
92
|
+
s.include?(':') ? Jets::Router::Route::CAPTURE_REGEX : s
|
93
93
|
end.join('\/')
|
94
94
|
# make sure beginning and end of the string matches
|
95
95
|
regexp_string = "^#{regexp_string}$"
|
@@ -18,6 +18,7 @@ module Jets::Controller::Middleware
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def call
|
21
|
+
ENV['JETS_HOST'] = jets_host # Dirty to use what's essentially a global variable but unsure how else to achieve
|
21
22
|
dup.call!
|
22
23
|
end
|
23
24
|
|
@@ -29,7 +30,7 @@ module Jets::Controller::Middleware
|
|
29
30
|
# Common setup logical at this point of middleware processing right before
|
30
31
|
# calling any controller actions.
|
31
32
|
def setup
|
32
|
-
# We already recreated a
|
33
|
+
# We already recreated a mimic rack env earlier as part of the very first
|
33
34
|
# middleware layer. However, by the time the rack env reaches the main middleware
|
34
35
|
# it could had been updated by other middlewares. We update the env here again.
|
35
36
|
@controller.request.set_env!(@env)
|
@@ -38,6 +39,11 @@ module Jets::Controller::Middleware
|
|
38
39
|
@controller.session = @env['rack.session'] || {}
|
39
40
|
end
|
40
41
|
|
42
|
+
def jets_host
|
43
|
+
default = "#{@env['rack.url_scheme']}://#{@env['HTTP_HOST']}"
|
44
|
+
Jets.config.helpers.host || default
|
45
|
+
end
|
46
|
+
|
41
47
|
def self.call(env)
|
42
48
|
instance = new(env)
|
43
49
|
instance.call
|
@@ -77,7 +77,7 @@ module Jets::Controller::Rack
|
|
77
77
|
#
|
78
78
|
# Passes a these special variables so we have access to them in the middleware.
|
79
79
|
# The controller instance is called in the Main middleware.
|
80
|
-
# The lambda.* info is used by the Rack::Local middleware to create a
|
80
|
+
# The lambda.* info is used by the Rack::Local middleware to create a mimiced
|
81
81
|
# controller for the local server.
|
82
82
|
#
|
83
83
|
def rack_vars(vars)
|
@@ -29,7 +29,7 @@ module Jets::Controller::Rack
|
|
29
29
|
'QUERY_STRING' => query_string,
|
30
30
|
'REMOTE_ADDR' => headers['X-Forwarded-For'],
|
31
31
|
'REMOTE_HOST' => headers['Host'],
|
32
|
-
'REQUEST_METHOD' => @event['httpMethod'],
|
32
|
+
'REQUEST_METHOD' => @event['httpMethod'] || 'GET', # useful to default to GET when testing with Lambda console
|
33
33
|
'REQUEST_PATH' => @event['path'],
|
34
34
|
'REQUEST_URI' => request_uri,
|
35
35
|
'SCRIPT_NAME' => "",
|
@@ -26,11 +26,12 @@ module Jets::Controller::Rendering
|
|
26
26
|
# x-jets-base64 to convert this Rack triplet to a API Gateway hash structure later
|
27
27
|
headers["x-jets-base64"] = base64 ? 'yes' : 'no' # headers values must be Strings
|
28
28
|
|
29
|
-
# Rails rendering does heavy lifting
|
30
29
|
if drop_content_info?(status)
|
31
30
|
body = StringIO.new
|
32
31
|
else
|
33
|
-
|
32
|
+
# Rails rendering does heavy lifting
|
33
|
+
# _prefixes provided by jets/overrides/rails/action_controller.rb
|
34
|
+
ActionController::Base._prefixes = @controller.controller_paths
|
34
35
|
renderer = ActionController::Base.renderer.new(renderer_options)
|
35
36
|
body = renderer.render(render_options)
|
36
37
|
body = StringIO.new(body)
|
@@ -39,16 +40,6 @@ module Jets::Controller::Rendering
|
|
39
40
|
[status, headers, body] # triplet
|
40
41
|
end
|
41
42
|
|
42
|
-
# Example: posts/index
|
43
|
-
def default_template_name
|
44
|
-
"#{template_namespace}/#{@controller.meth}"
|
45
|
-
end
|
46
|
-
|
47
|
-
# PostsController => "posts" is the namespace
|
48
|
-
def template_namespace
|
49
|
-
@controller.class.to_s.sub('Controller','').underscore.pluralize
|
50
|
-
end
|
51
|
-
|
52
43
|
# default options:
|
53
44
|
# https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/renderer.rb#L41-L47
|
54
45
|
def renderer_options
|
@@ -74,6 +65,41 @@ module Jets::Controller::Rendering
|
|
74
65
|
options
|
75
66
|
end
|
76
67
|
|
68
|
+
def render_options
|
69
|
+
# normalize the template option
|
70
|
+
template = @options[:template]
|
71
|
+
if template and !template.include?('/')
|
72
|
+
template = "#{template_namespace}/#{template}"
|
73
|
+
end
|
74
|
+
template ||= default_template_name
|
75
|
+
# ready to override @options[:template]
|
76
|
+
@options[:template] = template if @options[:template]
|
77
|
+
|
78
|
+
render_options = {
|
79
|
+
template: template, # weird: template needs to be set no matter because it
|
80
|
+
# sets the name which is used in lookup_context.rb:209:in `normalize_name'
|
81
|
+
layout: @options[:layout],
|
82
|
+
assigns: controller_instance_variables,
|
83
|
+
# prefixes: ["posts"],
|
84
|
+
}
|
85
|
+
types = %w[json inline plain file xml body action].map(&:to_sym)
|
86
|
+
types.each do |type|
|
87
|
+
render_options[type] = @options[type] if @options[type]
|
88
|
+
end
|
89
|
+
|
90
|
+
render_options
|
91
|
+
end
|
92
|
+
|
93
|
+
# Example: posts/index
|
94
|
+
def default_template_name
|
95
|
+
"#{template_namespace}/#{@controller.meth}"
|
96
|
+
end
|
97
|
+
|
98
|
+
# PostsController => "posts" is the namespace
|
99
|
+
def template_namespace
|
100
|
+
@controller.class.to_s.sub('Controller','').underscore.pluralize
|
101
|
+
end
|
102
|
+
|
77
103
|
# Takes headers and adds HTTP_ to front of the keys because that is what rack
|
78
104
|
# does to the headers passed from a request. This seems to be the standard
|
79
105
|
# when testing with curl and inspecting the headers in a Rack app. Example:
|
@@ -110,30 +136,7 @@ module Jets::Controller::Rendering
|
|
110
136
|
results
|
111
137
|
end
|
112
138
|
|
113
|
-
|
114
|
-
# nomralize the template option
|
115
|
-
template = @options[:template]
|
116
|
-
if template and !template.include?('/')
|
117
|
-
template = "#{template_namespace}/#{template}"
|
118
|
-
end
|
119
|
-
template ||= default_template_name
|
120
|
-
# ready to override @options[:template]
|
121
|
-
@options[:template] = template if @options[:template]
|
122
|
-
|
123
|
-
render_options = {
|
124
|
-
template: template, # weird: template needs to be set no matter because it
|
125
|
-
# sets the name which is used in lookup_context.rb:209:in `normalize_name'
|
126
|
-
layout: @options[:layout],
|
127
|
-
assigns: controller_instance_variables,
|
128
|
-
}
|
129
|
-
types = %w[json inline plain file xml body action].map(&:to_sym)
|
130
|
-
types.each do |type|
|
131
|
-
render_options[type] = @options[type] if @options[type]
|
132
|
-
end
|
133
|
-
|
134
|
-
render_options
|
135
|
-
end
|
136
|
-
|
139
|
+
# Pass controller instance variables from jets-based controller to ActionView scope
|
137
140
|
def controller_instance_variables
|
138
141
|
instance_vars = @controller.instance_variables.inject({}) do |vars, v|
|
139
142
|
k = v.to_s.sub(/^@/,'') # @var => var
|
@@ -141,6 +144,9 @@ module Jets::Controller::Rendering
|
|
141
144
|
vars
|
142
145
|
end
|
143
146
|
instance_vars[:event] = event
|
147
|
+
# jets internal variables
|
148
|
+
# So ActionView has access back to the jets controller
|
149
|
+
instance_vars[:_jets] = { controller: @controller }
|
144
150
|
instance_vars
|
145
151
|
end
|
146
152
|
|
@@ -199,7 +205,8 @@ module Jets::Controller::Rendering
|
|
199
205
|
# Assign local variable because scope in the `:action_view do` block changes
|
200
206
|
app_helper_classes = find_app_helper_classes
|
201
207
|
ActiveSupport.on_load :action_view do
|
202
|
-
include
|
208
|
+
include Jets::Router::Helpers # internal routes helpers
|
209
|
+
include ApplicationHelper # include first
|
203
210
|
app_helper_classes.each do |helper_class|
|
204
211
|
include helper_class
|
205
212
|
end
|
@@ -8,7 +8,8 @@ class Jets::Controller
|
|
8
8
|
return @url unless add_stage?
|
9
9
|
|
10
10
|
stage_name = Jets::Resource::ApiGateway::Deployment.stage_name
|
11
|
-
"/#{stage_name}#
|
11
|
+
stage_name_with_slashes = "/#{stage_name}/" # use to prevent stage name being added twice if url_for is called twice on the same string
|
12
|
+
@url.include?(stage_name_with_slashes) ? @url : "/#{stage_name}#{@url}"
|
12
13
|
end
|
13
14
|
|
14
15
|
def add_stage?
|
data/lib/jets/generator.rb
CHANGED
@@ -1,21 +1,85 @@
|
|
1
1
|
# Piggy back off of Rails Generators.
|
2
2
|
class Jets::Generator
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class << self
|
4
|
+
def invoke(generator, *args)
|
5
|
+
new(generator, *args).run(:invoke)
|
6
|
+
end
|
7
|
+
|
8
|
+
def revoke(generator, *args)
|
9
|
+
new(generator, *args).run(:revoke)
|
10
|
+
end
|
11
|
+
|
12
|
+
def help(args=ARGV)
|
13
|
+
require_generators
|
14
|
+
|
15
|
+
# `jets generate -h` results in:
|
16
|
+
#
|
17
|
+
# args = ["generate", "-h"]
|
18
|
+
#
|
19
|
+
args = args[1..-1] || []
|
20
|
+
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
21
|
+
args.pop if help_flags.include?(args.last)
|
22
|
+
subcommand = args[0]
|
23
|
+
|
24
|
+
out = capture_stdout do
|
25
|
+
if subcommand
|
26
|
+
# Using invoke because it ensure the generator is configured properly
|
27
|
+
invoke(subcommand) # sub-level: jets generate scaffold -h
|
28
|
+
else
|
29
|
+
puts Jets::Commands::Help.text(:generate) # to trigger the regular Thor help
|
30
|
+
# Note: How to call the original top-level help menu from Rails. Keeping around in case its useful later:
|
31
|
+
# Rails::Generators.help # top-level: jets generate -h
|
32
|
+
end
|
33
|
+
end
|
34
|
+
out.gsub('rails','jets').gsub('Rails','Jets')
|
35
|
+
end
|
6
36
|
|
7
|
-
|
8
|
-
|
37
|
+
def capture_stdout
|
38
|
+
stdout_old = $stdout
|
39
|
+
io = StringIO.new
|
40
|
+
$stdout = io
|
41
|
+
yield
|
42
|
+
$stdout = stdout_old
|
43
|
+
io.string
|
44
|
+
end
|
45
|
+
|
46
|
+
def require_generators
|
47
|
+
# lazy require so Rails const is only defined when using generators
|
48
|
+
require "rails/generators"
|
49
|
+
require "rails/configuration"
|
50
|
+
require_active_job_generator
|
51
|
+
end
|
52
|
+
|
53
|
+
def require_active_job_generator
|
54
|
+
require "active_job"
|
55
|
+
require "rails/generators/job/job_generator"
|
56
|
+
# Override the source_root
|
57
|
+
Rails::Generators::JobGenerator.class_eval do
|
58
|
+
def self.source_root
|
59
|
+
File.expand_path("../generator/templates/active_job/job/templates", __FILE__)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
9
63
|
end
|
10
64
|
|
11
65
|
def initialize(generator, *args)
|
12
66
|
@generator, @args = generator, args
|
67
|
+
@args << '--pretend' if noop?
|
68
|
+
end
|
69
|
+
|
70
|
+
# Used to delegate noop option to Rails generator pretend option. Both work:
|
71
|
+
#
|
72
|
+
# jets generate scaffold user title:string --noop
|
73
|
+
# jets generate scaffold user title:string --pretend
|
74
|
+
#
|
75
|
+
# Grabbing directly from the ARGV because think its cleaner than passing options from
|
76
|
+
# Thor all the way down.
|
77
|
+
def noop?
|
78
|
+
ARGV.include?('--noop')
|
13
79
|
end
|
14
80
|
|
15
81
|
def run(behavior=:invoke)
|
16
|
-
|
17
|
-
require "rails/generators"
|
18
|
-
require "rails/configuration"
|
82
|
+
self.class.require_generators
|
19
83
|
Rails::Generators.configure!(config)
|
20
84
|
Rails::Generators.invoke(@generator, @args, behavior: behavior, destination_root: Jets.root)
|
21
85
|
end
|
@@ -1,17 +1,12 @@
|
|
1
|
-
|
2
|
-
<%% action = editing ? "/<%= plural_table_name %>/#{<%= singular_table_name %>.id}" : "/<%= plural_table_name %>" %>
|
3
|
-
<%%= form_tag(action) do %>
|
4
|
-
<%% if editing -%>
|
5
|
-
<input type="hidden" name="_method" value="put" />
|
6
|
-
<%% end -%>
|
1
|
+
<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %>
|
7
2
|
<%% if <%= singular_table_name %>.errors.any? %>
|
8
3
|
<div id="error_explanation">
|
9
4
|
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
|
10
5
|
|
11
6
|
<ul>
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
<%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
|
8
|
+
<li><%%= message %></li>
|
9
|
+
<%% end %>
|
15
10
|
</ul>
|
16
11
|
</div>
|
17
12
|
<%% end %>
|
@@ -19,21 +14,21 @@
|
|
19
14
|
<% attributes.each do |attribute| -%>
|
20
15
|
<div class="field">
|
21
16
|
<% if attribute.password_digest? -%>
|
22
|
-
<%%=
|
23
|
-
<%%=
|
17
|
+
<%%= form.label :password %>
|
18
|
+
<%%= form.password_field :password %>
|
24
19
|
</div>
|
25
20
|
|
26
21
|
<div class="field">
|
27
|
-
<%%=
|
28
|
-
<%%=
|
22
|
+
<%%= form.label :password_confirmation %>
|
23
|
+
<%%= form.password_field :password_confirmation %>
|
29
24
|
<% else -%>
|
30
|
-
<%%=
|
31
|
-
<%%=
|
25
|
+
<%%= form.label :<%= attribute.column_name %> %>
|
26
|
+
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
|
32
27
|
<% end -%>
|
33
28
|
</div>
|
34
29
|
|
35
30
|
<% end -%>
|
36
31
|
<div class="actions">
|
37
|
-
<%%=
|
32
|
+
<%%= form.submit %>
|
38
33
|
</div>
|
39
34
|
<%% end %>
|