caterpillar 1.4.4 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/ChangeLog +27 -0
- data/MIT-LICENSE +0 -4
- data/README.rdoc +3 -3
- data/Rakefile +6 -22
- data/caterpillar.gemspec +28 -0
- data/generators/caterpillar/caterpillar_generator.rb +12 -4
- data/generators/caterpillar/templates/config/portlets.rb +10 -2
- data/generators/caterpillar/templates/stylesheets/portlet_test_bench/main.css +2 -2
- data/init.rb +20 -8
- data/lib/caterpillar.rb +33 -27
- data/lib/caterpillar/config.rb +3 -1
- data/lib/caterpillar/helpers/liferay.rb +129 -132
- data/lib/caterpillar/helpers/portlet.rb +41 -0
- data/lib/caterpillar/liferay.rb +2 -3
- data/lib/caterpillar/navigation.rb +2 -4
- data/lib/caterpillar/parser.rb +1 -5
- data/lib/caterpillar/portlet.rb +20 -19
- data/lib/caterpillar/portlet_support.rb +9 -22
- data/lib/caterpillar/security.rb +20 -42
- data/lib/caterpillar/task.rb +17 -36
- data/lib/caterpillar/usage.rb +14 -10
- data/lib/caterpillar/util.rb +1 -1
- data/lib/java/rails-portlet-0.12.0.jar +0 -0
- data/lib/rails_gem_chooser.rb +14 -7
- data/lib/web/portlet.rb +1 -1
- data/portlet_test_bench/controllers/caterpillar/application.rb +0 -5
- data/portlet_test_bench/controllers/caterpillar/junit_controller.rb +30 -9
- data/portlet_test_bench/controllers/caterpillar/liferay_controller.rb +13 -3
- data/portlet_test_bench/controllers/caterpillar/session_controller.rb +3 -0
- data/portlet_test_bench/controllers/caterpillar/xhr_controller.rb +12 -1
- data/portlet_test_bench/views/caterpillar/application/_back_to_menu.html.erb +1 -1
- data/portlet_test_bench/views/caterpillar/application/index.html.erb +8 -1
- data/portlet_test_bench/views/caterpillar/application/portlet_test_bench.html.erb +26 -8
- data/portlet_test_bench/views/caterpillar/css/background.html.erb +4 -1
- data/portlet_test_bench/views/caterpillar/css/simple.html.erb +13 -9
- data/portlet_test_bench/views/caterpillar/liferay/session_variables.html.erb +3 -12
- data/portlet_test_bench/views/caterpillar/session/cookies.html.erb +5 -0
- data/portlet_test_bench/views/caterpillar/session/namespace.html.erb +14 -0
- data/portlet_test_bench/views/caterpillar/xhr/resource.html.erb +15 -0
- data/portlet_test_bench/views/caterpillar/xhr/time.html.erb +7 -6
- data/spec/app1/config/routes.rb +3 -0
- data/spec/app2/config/routes.rb +7 -0
- data/spec/app3/config/routes.rb +5 -0
- data/spec/caterpillar/helper_spec.rb +69 -0
- data/spec/caterpillar/task_spec.rb +192 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +44 -0
- data/test/README +4 -0
- data/test/dtd/liferay-display_5_1_0.dtd +44 -44
- data/test/dtd/liferay-display_5_2_0.dtd +44 -44
- data/test/dtd/liferay-display_6_0_0.dtd +44 -44
- data/test/dtd/liferay-portlet-app_5_1_0.dtd +582 -582
- data/test/dtd/liferay-portlet-app_5_2_0.dtd +642 -642
- data/test/dtd/liferay-portlet-app_6_0_0.dtd +730 -730
- data/test/dtd/portlet-app_2_0.xsd +830 -830
- data/test/liferay_helpers_test.rb +94 -7
- data/test/portlet_support_test.rb +0 -18
- data/test/portlets_test.rb +6 -11
- data/test/xml_test.rb +4 -14
- metadata +53 -31
- data/lib/java/rails-portlet-0.10.1.jar +0 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2010 Mikael Lammmentausta, Tulio Ornelas dos Santos
|
4
|
+
#
|
5
|
+
# See the file MIT-LICENSE included with the distribution for
|
6
|
+
# software license details.
|
7
|
+
#++
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'action_controller'
|
11
|
+
|
12
|
+
module Caterpillar # :nodoc:
|
13
|
+
module Helpers # :nodoc:
|
14
|
+
module Portlet
|
15
|
+
|
16
|
+
# Get portlet namespace from cookie set by rails-portlet
|
17
|
+
def namespace_cookie
|
18
|
+
cookies[:Portlet_namespace]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set instance variable @namespace
|
22
|
+
def get_namespace
|
23
|
+
@namespace = namespace_cookie
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send the rendered page in a file to serveResource method
|
27
|
+
#
|
28
|
+
def ajax_response params = {}
|
29
|
+
if params[:template]
|
30
|
+
content = render_to_string :template => params[:template]
|
31
|
+
else
|
32
|
+
content = render_to_string
|
33
|
+
end
|
34
|
+
|
35
|
+
send_data resposta, :type => 'text/html', :filename => "content_#{request.session_options[:id]}.html"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
data/lib/caterpillar/liferay.rb
CHANGED
@@ -60,10 +60,9 @@ module Caterpillar # :nodoc:
|
|
60
60
|
attr_writer :deploy_dir
|
61
61
|
|
62
62
|
# Liferay version is given as a String, eg. '5.2.2'.
|
63
|
-
# Defaults to +Lportal::Schema.version+.
|
64
63
|
def initialize(version=nil)
|
65
|
-
@version = version || '
|
66
|
-
@root = '/usr/local/liferay-portal-
|
64
|
+
@version = version || '6.0.6'
|
65
|
+
@root = '/usr/local/liferay-portal-6.0.6/tomcat-6.0.29'
|
67
66
|
@server = 'Tomcat'
|
68
67
|
@deploy_dir = nil
|
69
68
|
end
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
|
-
|
4
2
|
#--
|
5
3
|
# (c) Copyright 2008,2009 Mikael Lammentausta
|
6
|
-
# See the file
|
4
|
+
# See the file MIT-LICENSE included with the distribution for
|
7
5
|
# software license details.
|
8
6
|
#++
|
9
7
|
|
@@ -44,4 +42,4 @@ module Caterpillar # :nodoc:
|
|
44
42
|
end
|
45
43
|
|
46
44
|
end
|
47
|
-
end
|
45
|
+
end
|
data/lib/caterpillar/parser.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
#--
|
3
3
|
# (c) Copyright 2008 Mikael Lammentausta
|
4
|
-
# See the file
|
4
|
+
# See the file MIT-LICENSE included with the distribution for
|
5
5
|
# software license details.
|
6
6
|
#++
|
7
7
|
|
@@ -121,11 +121,7 @@ module Caterpillar
|
|
121
121
|
### unless defined, use default javascripts
|
122
122
|
portlet[:javascripts] ||= @config.javascripts
|
123
123
|
|
124
|
-
# fix path variables to be replaced by rails-portlet at runtime
|
125
124
|
path = portlet[:path]
|
126
|
-
path.gsub!(/:uid/,'%UID%')
|
127
|
-
path.gsub!(/:gid/,'%GID%')
|
128
|
-
# TODO: notify user of unsupported variables
|
129
125
|
portlet.update( :path => path )
|
130
126
|
end
|
131
127
|
|
data/lib/caterpillar/portlet.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#--
|
3
3
|
# (c) Copyright 2008, 2010 Mikael Lammentausta
|
4
4
|
# 2010 Tulio Ornelas
|
5
|
-
# See the file
|
5
|
+
# See the file MIT-LICENSE included with the distribution for
|
6
6
|
# software license details.
|
7
7
|
#++
|
8
8
|
|
@@ -32,8 +32,9 @@ module Caterpillar
|
|
32
32
|
# Creates <portlet-app> XML document for portlet-ext.xml.
|
33
33
|
#
|
34
34
|
# @param portlets is an Array of Hashes
|
35
|
+
# @param session_secret is a generated String that authorizes requests from portlet clients
|
35
36
|
# @returns String
|
36
|
-
def xml(portlets)
|
37
|
+
def xml(portlets, session_secret=nil)
|
37
38
|
# create a new XML document
|
38
39
|
doc = REXML::Document.new
|
39
40
|
doc << REXML::XMLDecl.new('1.0', 'utf-8')
|
@@ -43,22 +44,22 @@ module Caterpillar
|
|
43
44
|
app.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'
|
44
45
|
app.attributes['xsi:schemaLocation'] = 'http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd'
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
47
|
+
unless session_secret
|
48
|
+
STDERR.puts %{WARNING: shared session secret not found from portlets.rb config.
|
49
|
+
This feature was added in version 2.0.0. Please update your config file:
|
50
|
+
|
51
|
+
portlet.session_secret = {
|
52
|
+
:key => '_rails_portlet',
|
53
|
+
:secret => '%s'
|
54
|
+
}
|
55
|
+
} % Security::random_secret
|
56
|
+
end
|
56
57
|
|
57
58
|
# create XML element tree
|
58
59
|
# (in proper order so the validation passes)
|
59
60
|
portlets.each do |portlet|
|
60
61
|
# <portlet>
|
61
|
-
app.elements << self.portlet_element(portlet,
|
62
|
+
app.elements << self.portlet_element(portlet, session_secret, app)
|
62
63
|
end
|
63
64
|
portlets.each do |portlet|
|
64
65
|
# <filter>
|
@@ -72,23 +73,23 @@ module Caterpillar
|
|
72
73
|
end
|
73
74
|
|
74
75
|
# <portlet> element.
|
75
|
-
#
|
76
|
-
def portlet_element(portlet,
|
76
|
+
# session_secret is a hash containing session key and secret.
|
77
|
+
def portlet_element(portlet, session_secret = nil, app = nil)
|
77
78
|
element = REXML::Element.new('portlet')
|
78
79
|
# NOTE: to pass validation, the elements need to be in proper order!
|
79
80
|
|
80
81
|
REXML::Element.new('portlet-name', element).text = portlet[:name]
|
81
82
|
REXML::Element.new('portlet-class', element).text = self.portlet_class
|
82
83
|
|
83
|
-
# insert
|
84
|
-
unless
|
84
|
+
# insert shared secret
|
85
|
+
unless session_secret.nil?
|
85
86
|
param = REXML::Element.new('init-param', element)
|
86
87
|
REXML::Element.new('name', param).text = 'session_key'
|
87
|
-
REXML::Element.new('value', param).text =
|
88
|
+
REXML::Element.new('value', param).text = session_secret[:key]
|
88
89
|
|
89
90
|
param = REXML::Element.new('init-param', element)
|
90
91
|
REXML::Element.new('name', param).text = 'secret'
|
91
|
-
REXML::Element.new('value', param).text =
|
92
|
+
REXML::Element.new('value', param).text = session_secret[:secret]
|
92
93
|
end
|
93
94
|
|
94
95
|
supports = REXML::Element.new('supports', element)
|
@@ -1,29 +1,16 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2010 Tulio Ornelas dos Santos
|
4
|
+
#
|
5
|
+
# See the file MIT-LICENSE included with the distribution for
|
6
|
+
# software license details.
|
7
|
+
#++
|
2
8
|
|
3
9
|
module Caterpillar
|
4
10
|
|
5
|
-
# Add some portlet support
|
11
|
+
# Add some portlet support (DEPRECATED)
|
6
12
|
#
|
7
13
|
module PortletSupport
|
8
|
-
|
9
|
-
# Gets portlet preferences from a cookie (Liferay_preferences) and generates
|
10
|
-
# a hash with it. Returns nil if cookie do not exists or the value is nil.
|
11
|
-
#
|
12
|
-
def get_liferay_preferences(value = cookies[:Liferay_preferences])
|
13
|
-
preferences = {}
|
14
|
-
if value and (not value.empty?)
|
15
|
-
value.split(";").each do |pair|
|
16
|
-
if pair.nil? or pair.empty? then next end
|
17
|
-
|
18
|
-
result = pair.split("=")
|
19
|
-
preferences[result[0].intern] = result[1]
|
20
|
-
|
21
|
-
end
|
22
|
-
return preferences
|
23
|
-
end
|
24
|
-
|
25
|
-
nil
|
26
|
-
end
|
27
|
-
|
14
|
+
#include Caterpillar::Helpers::Liferay
|
28
15
|
end
|
29
|
-
end
|
16
|
+
end
|
data/lib/caterpillar/security.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
|
-
|
4
2
|
#--
|
5
|
-
# (c) Copyright 2010 Mikael Lammentausta
|
3
|
+
# (c) Copyright 2010 - 2011 Mikael Lammentausta
|
6
4
|
#
|
7
5
|
# See the file MIT-LICENSE included with the distribution for
|
8
6
|
# software license details.
|
@@ -30,10 +28,18 @@ module Caterpillar # :nodoc:
|
|
30
28
|
base.extend(ClassMethods)
|
31
29
|
end
|
32
30
|
|
31
|
+
def self.random_secret(len=64)
|
32
|
+
chars = ('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a
|
33
|
+
(0...len).collect { chars[Kernel.rand(chars.length)] }.join
|
34
|
+
end
|
35
|
+
|
33
36
|
module ClassMethods # :nodoc:
|
37
|
+
# Inserts a before_filter that verifies user agent and checks the shared secret.
|
38
|
+
# Pass options for before_filter (eg. :only => [:secured_action])
|
34
39
|
def secure_portlet_sessions(options = {})
|
35
40
|
class_eval <<-EOV
|
36
41
|
include Caterpillar::Security::InstanceMethods
|
42
|
+
before_filter [:authorize_agent, :authorize_request], options
|
37
43
|
EOV
|
38
44
|
end
|
39
45
|
end
|
@@ -65,7 +71,12 @@ module Caterpillar # :nodoc:
|
|
65
71
|
# check the user agent
|
66
72
|
agent = request.env['HTTP_USER_AGENT']
|
67
73
|
unless agent=='Jakarta Commons-HttpClient/3.1'
|
68
|
-
|
74
|
+
addr = request.env['REMOTE_ADDR']
|
75
|
+
if addr[/^127/] and request.env['HTTP_X_FORWARDED_FOR']
|
76
|
+
# serving through virtualhost. obtain true IP addr.
|
77
|
+
addr = request.env['HTTP_X_FORWARDED_FOR']
|
78
|
+
end
|
79
|
+
logger.warn 'Someone from IP %s may be spoofing using agent %s' % [addr, agent]
|
69
80
|
render :nothing => true, :status => 404
|
70
81
|
end
|
71
82
|
end
|
@@ -77,7 +88,8 @@ module Caterpillar # :nodoc:
|
|
77
88
|
#
|
78
89
|
def authorize_request
|
79
90
|
if !cookies.nil? and !cookies[:session_secret].nil?
|
80
|
-
|
91
|
+
config = Util.eval_configuration
|
92
|
+
if cookies[:session_secret] == config.session_secret[:secret]
|
81
93
|
logger.debug "Passes security check"
|
82
94
|
return true
|
83
95
|
end
|
@@ -88,49 +100,15 @@ module Caterpillar # :nodoc:
|
|
88
100
|
end
|
89
101
|
|
90
102
|
|
91
|
-
#
|
103
|
+
# MOVED to Caterpillar::Helpers::Liferay
|
92
104
|
def get_liferay_uid
|
93
|
-
|
94
|
-
|
95
|
-
@uid = cookies[uid_key]
|
96
|
-
logger.debug("Liferay UID %s" % @uid)
|
97
|
-
else
|
98
|
-
logger.debug("UID key is not present in cookies %s" % cookies.inspect)
|
99
|
-
end
|
105
|
+
logger.warn 'DEPRECATION WARNING: get_liferay_uid has been moved to Caterpillar::Helpers::Liferay'
|
106
|
+
Caterpillar::Helpers::Liferay.get_liferay_uid
|
100
107
|
end
|
101
108
|
|
102
109
|
|
103
110
|
end # module
|
104
111
|
|
105
112
|
|
106
|
-
# Return Rails' session key
|
107
|
-
def self.get_session_key
|
108
|
-
# Rails before 2.3 had a different way
|
109
|
-
if defined?(RAILS_GEM_VERSION) and RAILS_GEM_VERSION.gsub('.','').to_i < 230
|
110
|
-
ActionController::Base.session_options_for(nil,nil)[:session_key]
|
111
|
-
# On Rails 2.3:
|
112
|
-
else
|
113
|
-
key = ActionController::Base.session_options[:key]
|
114
|
-
return key unless key.nil?
|
115
|
-
# try session_key
|
116
|
-
key = ActionController::Base.session_options[:session_key]
|
117
|
-
return key unless key.nil?
|
118
|
-
# XXX: Rails changed sometime during 2.3.8 ..
|
119
|
-
STDERR.puts 'Failed to read session key - consider this a bug'
|
120
|
-
return nil
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# Return Rails' secret
|
125
|
-
def self.get_secret
|
126
|
-
# Rails before 2.3 had a different way
|
127
|
-
if RAILS_GEM_VERSION.gsub('.','').to_i < 230
|
128
|
-
ActionController::Base.session_options_for(nil,nil)[:secret]
|
129
|
-
# On Rails 2.3:
|
130
|
-
else
|
131
|
-
ActionController::Base.session_options[:secret]
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
113
|
end
|
136
114
|
end
|
data/lib/caterpillar/task.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
#--
|
3
|
-
# (c) Copyright 2008
|
3
|
+
# (c) Copyright 2008 - 2011 Mikael Lammentausta
|
4
4
|
#
|
5
5
|
# Thanks to Nick Sieger for the rake structure!
|
6
6
|
#
|
7
|
-
# See the file
|
7
|
+
# See the file MIT-LICENSE included with the distribution for
|
8
8
|
# software license details.
|
9
9
|
#++
|
10
10
|
|
@@ -84,7 +84,7 @@ module Caterpillar
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def define_usage_task
|
87
|
-
task :usage do
|
87
|
+
task :usage => :version do
|
88
88
|
Usage.show
|
89
89
|
end
|
90
90
|
end
|
@@ -217,12 +217,13 @@ module Caterpillar
|
|
217
217
|
raise 'Rails environment could not be loaded'
|
218
218
|
end
|
219
219
|
if @config.container.is_a?(Caterpillar::Liferay)
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
220
|
+
# since Caterpillar version 1.6.0, Liferay version is not required
|
221
|
+
#if @config.container.version.nil? and !defined?(Lportal)
|
222
|
+
# $stderr.puts 'Liferay version is undefined, and lportal gem is not present.'
|
223
|
+
# $stderr.puts 'Please define portlet.container.version in %s.' % @config.class::FILE
|
224
|
+
# raise 'Insufficient configuration'
|
225
|
+
#end
|
226
|
+
#@config.container.version ||= Lportal::Schema.version
|
226
227
|
portal_info
|
227
228
|
end
|
228
229
|
end
|
@@ -253,7 +254,7 @@ module Caterpillar
|
|
253
254
|
|
254
255
|
FileUtils.touch(file)
|
255
256
|
f=File.open(file,'w')
|
256
|
-
f.write Portlet.xml(@portlets)
|
257
|
+
f.write Portlet.xml(@portlets,@config.session_secret)
|
257
258
|
f.close
|
258
259
|
#info '-> %s' % file
|
259
260
|
end
|
@@ -362,31 +363,12 @@ module Caterpillar
|
|
362
363
|
with_namespace_and_config do |name, config|
|
363
364
|
desc 'Installs Rails-portlet JAR into the portlet container'
|
364
365
|
task :install => :environment do
|
365
|
-
source = File.join(CATERPILLAR_LIBS,'java')
|
366
|
-
|
367
|
-
# detect (Liferay) container version
|
368
|
-
container_v = @config.container.version
|
369
|
-
unless container_v
|
370
|
-
info 'Unable to detect the version of the portlet container. Installing the latest version.'
|
371
|
-
end
|
372
366
|
|
373
|
-
#
|
374
|
-
|
375
|
-
#
|
376
|
-
|
377
|
-
|
378
|
-
# '0.6.0' # FIXME: branch properly
|
379
|
-
# else
|
380
|
-
# '0.10.0'
|
381
|
-
# end
|
382
|
-
#)
|
383
|
-
version = '0.10.1'
|
384
|
-
require 'find'
|
385
|
-
Find.find(source) do |file|
|
386
|
-
if File.basename(file) == "rails-portlet-#{version}.jar"
|
387
|
-
portlet_jar = file
|
388
|
-
end
|
389
|
-
end
|
367
|
+
# since rails-portlet jar version 0.12, Liferay5 and Liferay6 both work
|
368
|
+
# with the same version. version detection is unneeded.
|
369
|
+
# container_v = @config.container.version
|
370
|
+
version = '0.12.0'
|
371
|
+
portlet_jar = File.join(CATERPILLAR_LIBS,'java',"rails-portlet-#{version}.jar")
|
390
372
|
|
391
373
|
# check if requirements match
|
392
374
|
unless deployment_requirements_met?
|
@@ -672,10 +654,9 @@ module Caterpillar
|
|
672
654
|
end
|
673
655
|
|
674
656
|
def portal_info(config=@config)
|
675
|
-
msg = 'Caterpillar %s configured for %s
|
657
|
+
msg = 'Caterpillar %s configured for %s at %s' % [
|
676
658
|
Caterpillar::VERSION,
|
677
659
|
config.container.name,
|
678
|
-
config.container.version,
|
679
660
|
config.container.root
|
680
661
|
]
|
681
662
|
info(msg)
|
data/lib/caterpillar/usage.rb
CHANGED
@@ -1,21 +1,25 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
3
|
-
|
4
2
|
#--
|
5
|
-
# (c) Copyright 2008 Mikael Lammentausta
|
6
|
-
# See the file
|
3
|
+
# (c) Copyright 2008, 2010 Mikael Lammentausta
|
4
|
+
# See the file MIT-LICENSE included with the distribution for
|
7
5
|
# software license details.
|
8
6
|
#++
|
9
7
|
|
10
8
|
module Caterpillar
|
11
9
|
class Usage
|
12
10
|
def self.show
|
13
|
-
STDOUT.puts
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
STDOUT.puts %{
|
12
|
+
Usage:
|
13
|
+
caterpillar --describe # gives you an overview of the tasks
|
14
|
+
|
15
|
+
For more information on usage in your Rails-portlet project,
|
16
|
+
see the README in the gem or at http://github.com/lamikae/caterpillar
|
17
|
+
}
|
18
|
+
# XXX: write better usage for stdout
|
19
|
+
=begin
|
20
|
+
To start up a new JRuby Rails-portlet project:
|
21
|
+
caterpillar rails project_name
|
22
|
+
=end
|
19
23
|
end
|
20
24
|
end
|
21
25
|
end
|