Ruby on Rails
RailsProductGenerator (Version #20)

Author: DuaneJohnson (canadaduane)
License: Ruby License

What is a ‘Productized Rails Application’ ?

A “productized” application is a two-tiered application. The first tier is a single base application that encapsulates common functionality for your product. The second tier consists of several customized applications that inherit and extend functionality from that base. It is useful for web developers who want to create a base application with some sort of generic functionality (e.g. a shopping cart) and then extend or modify that functionality for specific clients only.

The rails_product shell command (available in gem form below) generates the entire Rails directory structure with some modifications as required by the aims of a Productized Rails Application.

See the original post for more information.

Installation

NOTE: rails_product and productize are not compatible with rails 1.0.

Question: Why is this? Where do I start to bring this up to date?

To install the rails_product generator run:

$ gem install site_generator

When it asks you if you want to install its dependency, ‘rails_product’, choose Yes.

(The installation above assumes you have the prerequisites to Rails already installed, e.g. Ruby 1.8.2 2004-12-25 and the ‘gem’ shell command from rubyforge.org, as well as Rails 0.13.1 itself).

So, now that you’ve got the gems installed, here’s how you’d create your very first Rails product:

<pre> $ cd ~/Projects $ rails_product shopping_cart create create app/apis <snip> create sites create Rakefile create README create CHANGELOG create app/controllers/application.rb create app/helpers/application_helper.rb create test/test_helper.rb create config/database.yml create config/routes.rb create public/.htaccess create sites/Rakefile <snip> $ cd shopping_cart $ ./script/generate site my_first_cart_client create sites/my_first_cart_client/app/controllers create sites/my_first_cart_client/app/helpers create sites/my_first_cart_client/app/models create sites/my_first_cart_client/app/views/layouts create sites/my_first_cart_client/db/migrate create sites/my_first_cart_client/log create sites/my_first_cart_client/public create sites/my_first_cart_client/config create sites/my_first_cart_client/test/fixtures create sites/my_first_cart_client/test/functional create sites/my_first_cart_client/test/unit create sites/my_first_cart_client/public/.htaccess create sites/my_first_cart_client/public/dispatch.fcgi create sites/my_first_cart_client/config/routes.rb $ ./script/server -s my_first_cart_client => Productized Rails application started on <a href="http://0.0.0.0">http://0.0.0.0</a>:3000 ... </pre>

At this point, assuming your application is going to be using a
database, you’ll want to go make your first site’s database. Depending on how you do things, you’ll want to make one, two, or possibly all three of these databases:

  • my_first_cart_client_dev
  • my_first_cart_client_test
  • my_first_cart_client

The last database in that list is the production database.

How Does The Directory Structure Differ From a Regular Rails Application?

The directory structure of the Generic application is just like a normal Rails application, with one additional directory: the “sites” directory. So it looks something like this:

<pre> RAILS_ROOT/ app/ controllers/ helpers/ models/ views/ public/ sites/ best_ever_adoptions_co_inc/ # <-- Site #1 app/ controllers/ helpers/ models/ views/ config/ db/ migrate/ public/ test/ fixtures/ functional/ unit/ yet_another_adoption_co/ # <-- Site #2 app/ controllers/ helpers/ models/ views/ (etc.) </pre>

Note that although the directory structure of each individual site is almost identical to a regular Rails application, you need not have any files in these directories. In fact, you can omit any directory (except the SITE_ROOT, e.g. sites/ best_ever_adoptions_co_inc/) if it does not contain files. So how does it work?

Well, if you put a file called, for example, welcome_controller.rb in RAILS_ROOT/app/controllers, then all of your sites will access that controller as normal UNLESS you put a file with the same name in the site-specific controllers folder. In that case, the RAILS_ROOT welcome_controller.rb will be loaded, and then the WelcomeController? class will be reopened (yay for Ruby!) by the site-specific welcome_controller.rb (say, for example, in RAILS_ROOT/sites/yet_another_adoption_co/app/controllers/) and modified as needed. For example, you could override the “index” method to do a redirect, or you could add a before_filter to make the controller require a login.

Views, layouts and partials act similarly—however, they simply override their generic counterparts. For example, if you have an index.rhtml and a title.rhtml in RAILSROOT/app/views/welcome/ and supposing this index.rhtml has a <%= render :partial => “title” %> in its code, then the title partial will be rendered in place. Now, however, let’s say that we put a _title.rhtml file in RAILSROOT/sites/yet_another_adoption_co/app/views/welcome/. In that case, our generic index.rhtml will load the site-specific _title partial in place. Hierarchical customizations in a jiffy.

Implementation Details

If you would like to understand how all of the pieces work together to accomplish this feat, read on.

Here is what the rails_product generator will do:

  1. Create the RAILS_ROOT directory structure as usual, with the addition of a /sites folder.
  2. Create a productize.rb file in your application’s lib/ folder.(Open it and read the comments to see what it does exactly. This is where the core magic happens since we can re-open the Rails classes as necessary to allow for our productization approach.)
  3. Create a slightly modified environment.rb file in your config/ folder. The following three lines are different from the regular environment.rb:
    1. SITE = ENVSITE’ || ENV‘site’ – Sets a constant for the rest of the application so it can know where to find your site’s stuff.
    2. require ‘productize’ – Modifies the core Rails classes as necessary to load both the base application’s code and the site-specific code.
    3. File.join(SITE_ROOT, ‘app’, ’controllers’) – Lets the Dependencies::LoadingModules class know where to find site-specific controllers if the base controller doesn’t exist.
  4. Create a modified database.yml file that will dynamically load the correct database when the server starts up.
  5. Create an ‘apache’ directory in your config/ folder with a sample Apache VirtualHost configuration file. This sample file will show you what you’ll need to do to get Apache to behave like WEBrick below (shares resources).
  6. Create a modified script/server file that will let your WEBrick server share the resources in your base application’s public folder (e.g. images, javascripts etc.) without having to copy them to each site’s individual public folder (Hurray for the DRY principle!) (DRY=Don’t Repeat Yourself).

—-
I had issues with this setup working if you had folder structures under (for instance) RAILS_ROOT/public/images which you wanted the sites to transparently have access to. Mod_rewrite was not matching the request and trying to pass it on to dispatch.fcgi.

Here’s a (lightly tested, YMMV) modification hack to the site-specific .htaccess file that appears to solve my problem…


# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On
RewriteRule ^$ index.html [QSA]

# Hack to fix mod_rewrite bug: we catch the incoming uri as an environment variable 'DOC'
RewriteRule "(.*)" "$1" [env=DOC:$1]

# If the requested file does not exist in SITE_ROOT/public...
RewriteCond %{DOCUMENT_ROOT}%{ENV:DOC} !-f

# ... then split its full path up in to manageable pieces ...
RewriteCond %{REQUEST_FILENAME} ^(.*)/sites/.+$

# ... and check to see if the file exists in the RAILS_ROOT/public folder...
RewriteCond %1/public/%{ENV:DOC} -f

# ... if so, rewrite our requested file to be the RAILS_ROOT one
RewriteRule ^(.*)$ /generic/%{ENV:DOC} [NS,L]
RewriteRule ^([^.]+)$ $1.html [QSA] 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]

solo & canadaduane

How is this going to work in 1.0 (0.14.x)? — Mischa
Yea, I wonder how branching solutions like this get affected by the rails upgrades.

—————————————-
I am using ruby 1.8.3 and cannot get rails_product to work because of:
/usr/local/lib/ruby/gems/1.8/gems/activesupport-1.1.1/lib/active_support/clean_logger.rb:13:in
`remove_const’: constant Logger::Format not defined (NameError)

Did a google search and here is the answer: http://groups.google.com/group/rubyonrails/browse_thread/thread/a3ce774773d9b5ec
There’s a simple fix for this. Just replace line 13 of active_support/clean_logger.rb from this:
remove_const “Format”
to this:
remove_const “Format” if const_defined? “Format”
—————————————-
Also, in site_generator 0.6, when you first do:
./script/generate site my_first_cart_client
it will fail as:
./script/../config/../lib/productize.rb:1:in `join’: can’t convert nil into String (TypeError)
from ./script/../config/../lib/productize.rb:1
from /usr/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in
`require’
from /usr/local/lib/ruby/gems/1.8/gems/activesupport-1.2.3/lib/active_su
pport/dependencies.rb:214:in `require’
from ./script/../config/environment.rb:66
from ./script/generate:2

because ENVSITE’ is not defined. I added this to the first line of lib/productize.rb to by pass it.
SITE=’’ if (SITE == nil)

—————————————-
I found that line 24 in productize.rb need to be changed from:

site_specific_path = File.join(SITE_ROOT, ‘app’, ‘views’, template_path + “.” + extension)

to

site_specific_path = File.join(SITE_ROOT, ‘app’, ‘views’, template_path + “.” + extension.to_s)

in order to load

Author: DuaneJohnson (canadaduane)
License: Ruby License

What is a ‘Productized Rails Application’ ?

A “productized” application is a two-tiered application. The first tier is a single base application that encapsulates common functionality for your product. The second tier consists of several customized applications that inherit and extend functionality from that base. It is useful for web developers who want to create a base application with some sort of generic functionality (e.g. a shopping cart) and then extend or modify that functionality for specific clients only.

The rails_product shell command (available in gem form below) generates the entire Rails directory structure with some modifications as required by the aims of a Productized Rails Application.

See the original post for more information.

Installation

NOTE: rails_product and productize are not compatible with rails 1.0.

Question: Why is this? Where do I start to bring this up to date?

To install the rails_product generator run:

$ gem install site_generator

When it asks you if you want to install its dependency, ‘rails_product’, choose Yes.

(The installation above assumes you have the prerequisites to Rails already installed, e.g. Ruby 1.8.2 2004-12-25 and the ‘gem’ shell command from rubyforge.org, as well as Rails 0.13.1 itself).

So, now that you’ve got the gems installed, here’s how you’d create your very first Rails product:

<pre> $ cd ~/Projects $ rails_product shopping_cart create create app/apis <snip> create sites create Rakefile create README create CHANGELOG create app/controllers/application.rb create app/helpers/application_helper.rb create test/test_helper.rb create config/database.yml create config/routes.rb create public/.htaccess create sites/Rakefile <snip> $ cd shopping_cart $ ./script/generate site my_first_cart_client create sites/my_first_cart_client/app/controllers create sites/my_first_cart_client/app/helpers create sites/my_first_cart_client/app/models create sites/my_first_cart_client/app/views/layouts create sites/my_first_cart_client/db/migrate create sites/my_first_cart_client/log create sites/my_first_cart_client/public create sites/my_first_cart_client/config create sites/my_first_cart_client/test/fixtures create sites/my_first_cart_client/test/functional create sites/my_first_cart_client/test/unit create sites/my_first_cart_client/public/.htaccess create sites/my_first_cart_client/public/dispatch.fcgi create sites/my_first_cart_client/config/routes.rb $ ./script/server -s my_first_cart_client => Productized Rails application started on <a href="http://0.0.0.0">http://0.0.0.0</a>:3000 ... </pre>

At this point, assuming your application is going to be using a
database, you’ll want to go make your first site’s database. Depending on how you do things, you’ll want to make one, two, or possibly all three of these databases:

  • my_first_cart_client_dev
  • my_first_cart_client_test
  • my_first_cart_client

The last database in that list is the production database.

How Does The Directory Structure Differ From a Regular Rails Application?

The directory structure of the Generic application is just like a normal Rails application, with one additional directory: the “sites” directory. So it looks something like this:

<pre> RAILS_ROOT/ app/ controllers/ helpers/ models/ views/ public/ sites/ best_ever_adoptions_co_inc/ # <-- Site #1 app/ controllers/ helpers/ models/ views/ config/ db/ migrate/ public/ test/ fixtures/ functional/ unit/ yet_another_adoption_co/ # <-- Site #2 app/ controllers/ helpers/ models/ views/ (etc.) </pre>

Note that although the directory structure of each individual site is almost identical to a regular Rails application, you need not have any files in these directories. In fact, you can omit any directory (except the SITE_ROOT, e.g. sites/ best_ever_adoptions_co_inc/) if it does not contain files. So how does it work?

Well, if you put a file called, for example, welcome_controller.rb in RAILS_ROOT/app/controllers, then all of your sites will access that controller as normal UNLESS you put a file with the same name in the site-specific controllers folder. In that case, the RAILS_ROOT welcome_controller.rb will be loaded, and then the WelcomeController? class will be reopened (yay for Ruby!) by the site-specific welcome_controller.rb (say, for example, in RAILS_ROOT/sites/yet_another_adoption_co/app/controllers/) and modified as needed. For example, you could override the “index” method to do a redirect, or you could add a before_filter to make the controller require a login.

Views, layouts and partials act similarly—however, they simply override their generic counterparts. For example, if you have an index.rhtml and a title.rhtml in RAILSROOT/app/views/welcome/ and supposing this index.rhtml has a <%= render :partial => “title” %> in its code, then the title partial will be rendered in place. Now, however, let’s say that we put a _title.rhtml file in RAILSROOT/sites/yet_another_adoption_co/app/views/welcome/. In that case, our generic index.rhtml will load the site-specific _title partial in place. Hierarchical customizations in a jiffy.

Implementation Details

If you would like to understand how all of the pieces work together to accomplish this feat, read on.

Here is what the rails_product generator will do:

  1. Create the RAILS_ROOT directory structure as usual, with the addition of a /sites folder.
  2. Create a productize.rb file in your application’s lib/ folder.(Open it and read the comments to see what it does exactly. This is where the core magic happens since we can re-open the Rails classes as necessary to allow for our productization approach.)
  3. Create a slightly modified environment.rb file in your config/ folder. The following three lines are different from the regular environment.rb:
    1. SITE = ENVSITE’ || ENV‘site’ – Sets a constant for the rest of the application so it can know where to find your site’s stuff.
    2. require ‘productize’ – Modifies the core Rails classes as necessary to load both the base application’s code and the site-specific code.
    3. File.join(SITE_ROOT, ‘app’, ’controllers’) – Lets the Dependencies::LoadingModules class know where to find site-specific controllers if the base controller doesn’t exist.
  4. Create a modified database.yml file that will dynamically load the correct database when the server starts up.
  5. Create an ‘apache’ directory in your config/ folder with a sample Apache VirtualHost configuration file. This sample file will show you what you’ll need to do to get Apache to behave like WEBrick below (shares resources).
  6. Create a modified script/server file that will let your WEBrick server share the resources in your base application’s public folder (e.g. images, javascripts etc.) without having to copy them to each site’s individual public folder (Hurray for the DRY principle!) (DRY=Don’t Repeat Yourself).

—-
I had issues with this setup working if you had folder structures under (for instance) RAILS_ROOT/public/images which you wanted the sites to transparently have access to. Mod_rewrite was not matching the request and trying to pass it on to dispatch.fcgi.

Here’s a (lightly tested, YMMV) modification hack to the site-specific .htaccess file that appears to solve my problem…


# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On
RewriteRule ^$ index.html [QSA]

# Hack to fix mod_rewrite bug: we catch the incoming uri as an environment variable 'DOC'
RewriteRule "(.*)" "$1" [env=DOC:$1]

# If the requested file does not exist in SITE_ROOT/public...
RewriteCond %{DOCUMENT_ROOT}%{ENV:DOC} !-f

# ... then split its full path up in to manageable pieces ...
RewriteCond %{REQUEST_FILENAME} ^(.*)/sites/.+$

# ... and check to see if the file exists in the RAILS_ROOT/public folder...
RewriteCond %1/public/%{ENV:DOC} -f

# ... if so, rewrite our requested file to be the RAILS_ROOT one
RewriteRule ^(.*)$ /generic/%{ENV:DOC} [NS,L]
RewriteRule ^([^.]+)$ $1.html [QSA] 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]

solo & canadaduane

How is this going to work in 1.0 (0.14.x)? — Mischa
Yea, I wonder how branching solutions like this get affected by the rails upgrades.

—————————————-
I am using ruby 1.8.3 and cannot get rails_product to work because of:
/usr/local/lib/ruby/gems/1.8/gems/activesupport-1.1.1/lib/active_support/clean_logger.rb:13:in
`remove_const’: constant Logger::Format not defined (NameError)

Did a google search and here is the answer: http://groups.google.com/group/rubyonrails/browse_thread/thread/a3ce774773d9b5ec
There’s a simple fix for this. Just replace line 13 of active_support/clean_logger.rb from this:
remove_const “Format”
to this:
remove_const “Format” if const_defined? “Format”
—————————————-
Also, in site_generator 0.6, when you first do:
./script/generate site my_first_cart_client
it will fail as:
./script/../config/../lib/productize.rb:1:in `join’: can’t convert nil into String (TypeError)
from ./script/../config/../lib/productize.rb:1
from /usr/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in
`require’
from /usr/local/lib/ruby/gems/1.8/gems/activesupport-1.2.3/lib/active_su
pport/dependencies.rb:214:in `require’
from ./script/../config/environment.rb:66
from ./script/generate:2

because ENVSITE’ is not defined. I added this to the first line of lib/productize.rb to by pass it.
SITE=’’ if (SITE == nil)

—————————————-
I found that line 24 in productize.rb need to be changed from:

site_specific_path = File.join(SITE_ROOT, ‘app’, ‘views’, template_path + “.” + extension)

to

site_specific_path = File.join(SITE_ROOT, ‘app’, ‘views’, template_path + “.” + extension.to_s)

in order to load