s

Create your own custom rails generator

Feb 22, 2009  -  Comments

It's really easy to add a custom generator to your Rails application. Say you have a component you want to include in multiple projects, but you don't want to manually copy ALL of the files from project to project. At Plexus, we have an empty Rails project with basic styling and structure that we use for all new applications. We have several components that we wanted to simplify adding to new projects. So, we created a few custom generators that we can use to create the components with very little effort.

The first thing you need to do is add a generators folder inside the lib folder. In there you can add the files and folders for each custom generator. In this example, I'll use a Blog as the component I'm building a generator for.

Inside the generators folder, I created a blog folder (hint: whatever you name the folder will be how you call your custom generator). All of my files for the blog functionality will be in this folder. The two most important things in this folder are the actual generator file that will do all of the work and the templates folder which contains all the files to be copied. My blog generator file, blog_generator.rb looks like this:

class BlogGenerator < Rails::Generator::Base
  def manifest
    record do |m|

      # Controllers
      m.file "controllers/blog_controller.rb", "app/controllers/blog_controller.rb"

      # Models
      m.file "models/blog_post.rb", "app/models/blog_post.rb"

      # Helpers
      m.file "helpers/blog_helper.rb", "app/helpers/blog_helper.rb"

      # Views
      m.directory "app/views/blog"
      m.file "views/index.html.erb", "app/views/blog/index.html.erb"
      m.file "views/details.html.erb", "app/views/blog/details.html.erb"
      m.file "views/feed.rss.builder", "app/views/blog/feed.rss.builder"

      # Migration
      m.migration_template "migrate/create_blog.rb", "db/migrate"

      # Tests
      m.file "test/fixtures/blog_posts.yml", "test/fixtures/blog_posts.yml"
      m.file "test/functional/blog_controller_test.rb", "test/functional/blog_controller_test.rb"
      m.file "test/unit/blog_post_test.rb", "test/unit/blog_post_test.rb"

      # CSS and images
      m.file "/4/assets/blog_styles.css", "/4/public/stylesheets/px_blogger.css"
      m.file "/4/assets/comment_add.gif", "/4/public/images/comment_add.gif"
      m.file "/4/assets/comment.gif", "/4/public/images/comment.gif"

      m.readme "INSTALL"
    end
  end

  def file_name
    "create_blog"
  end

end

Here is a breakdown of what is going on:

  • The directory method will create the specified directory if it doesn't exist already.
  • The file method will copy the specified file to the given directory.
  • The migration_template file will copy the given migration file into the db/migrations folder using the file_name method defined at the bottom of the generator to name the file.
  • The readme function prints out the contents of the INSTALL file after the generator script is called. You can use this file to put any extra instructions for the generator.

This is what the file structure looks like for the generator:

lib
\- generators
   \- blog
      \- blog_generator.rb
         templates
         \- assets
            \- blog_styles.css
               comment_add.gif
               comment.gif
            controllers
            \- blog_controller.rb
            helpers
            \- blog_helper.rb
            INSTALL
            migrate
            \- create_blog.rb
            models
            \- blog_post.rb
            test
            \- fixtures
               \- blog_posts.yml
               functional
               \- blog_controller_test.rb
               unit
               \- blog_post_test.rb
            views
            \- index.html.erb
               details.html.erb
               feed.rss.builder
         USAGE

All we need to do to run this generator is call script/generate blog.

Tagged: railsgeneratorstutorial

Deploying a Merb application with the RailsMachine gem

Feb 07, 2009  -  Comments

I recently launched a monthly bill/task tracking application I've been working on in my spare time. I used Merb so I could get some experience with the framework.

Plexus was kind enough to donate some server space on a RailsMachine server. Luckily, they recently added Passenger support to their awesome RailsMachine gem, so all I needed was to add a Rack config file to run my app on Passenger.

require 'rubygems'
require 'merb-core'
 
Merb::Config.setup(:merb_root => File.expand_path(File.dirname(__FILE__)),
                   :environment => ENV['RACK_ENV'])
Merb.environment = Merb::Config[:environment]
Merb.root = Merb::Config[:merb_root]
Merb::BootLoader.run
 
run Merb::Rack::Application.new

After that, I only needed to update the Capistrano deploy file to work with Merb and Passenger.

require 'railsmachine/recipes'
 
# The name of your application. Used for directory and file names associated with
# the application.
set :application, "listode"
 
# Target directory for the application on the web and app servers.
set :deploy_to, "/var/www/apps/#{application}"
 
# Primary domain name of your application. Used as a default for all server roles.
set :domain, "listode.com"
 
# Login user for ssh.
set :user, "deploy"
set :runner, user
set :admin_runner, user
 
# Rails environment. Used by application setup tasks and migrate tasks.
set :rails_env, "production"
 
# Automatically symlink these directories from curent/public to shared/public.
set :app_symlinks, %w{graphs}
 
set :deploy_via, :remote_cache
 
# =============================================================================
# ROLES
# =============================================================================
# Modify these values to execute tasks on a different server.
role :web, domain
role :app, domain
role :db, domain, :primary => true
role :scm, domain
 
 
# =============================================================================
# APPLICATION SERVER OPTIONS
# =============================================================================
set :app_server, :passenger # :mongrel or :passenger
 
# =============================================================================
# SCM OPTIONS
# =============================================================================
set :scm, :git # :subversion or :git
set :repository, "git@github.com:travisr/#{application}.git"
 
# =============================================================================
# CUSTOM CONFIGURATION
# =============================================================================
# action to symlink database file
namespace :deploy do
  desc "Symlink database config file."
  task :symlink_db do
    run "ln -nfs #{shared_path}/system/database.yml #{release_path}/config/database.yml"
  end
end
 
# Overwrite the default deploy.migrate as it calls:
# rake RAILS_ENV=production db:migrate
desc "Use datamapper to call autoupgrade instead of db:migrate."
deploy.task :migrate do
 run "cd #{release_path}; rake db:autoupgrade MERB_ENV=production"
end

after 'deploy:update_code', 'deploy:symlink_db'

The custom section at the bottom sets up a symlink to my databases.yml file since I don't keep that in my git repo. I also have to override the migration action to use DataMapper's db:autoupgrade.

Tagged: rails machinecapistranopassengermerbtutorial

Changing Paperclip File Storage Location

Jan 11, 2009  -  Comments

For a while at Plexus, we've been using FileColumn for all our image/file upload attachment needs. It's worked out really well, but when we saw Paperclip we thought it might be a better choice.

Paperclip is super easy to setup and use, but we found ourselves wanting to slightly change the default way it stored attachments. We were used to the way that FileColumn created its folder structure. It would make a folder named for the model (singular) that the attachment(s) were part of and a folder for each attachment (singular). Say we had a BlogPost model with an image attachment and a file attachment. FileColumn would make the following two folders: public/blog_post/image and public/blog_post/file.

Paperclip operates a little differently. It creates a folder (plural) in public for each attachment. If you had the same setup as before, Paperclip would create the following two folders: public/images and public/files. Can you see the immediate problem with this? We already have a public/images folder in our default Rails file structure, so this might get a little confusing. Another problem arises if we have several models with an image attachment. Paperclip would store them all in the same folder. This would be ok 99% of the time. See, Paperclip gets images by their id and name, so even if two models have image attachments and the same id, as long as the name is different, we're ok. But, if you somehow add a different image with the same name, to a model with the same id, then it would get overwritten. Not very likely, but still possible.

The great thing about Paperclip is that you can change the default way it stores its attachments. You can just past a few extra options to the has_attached_file call in your model. As of the current version of Paperclip, they have fixed the file structure problem by tweaking the default storage path. They added a system subfolder in the public folder. This has a two-fold benefit. First, we don't have the problem before of having an attachment named image. Second, this works great for Capistrano because the system folder is already symlinked from the public folder, so you don't have to worry about adding symlinks to your deploy file.

Anyway, back to my point of changing the default file storage path. You can just add the url (which tells where to retrieve the files) and path (which tells where to save the file) options to your has_attached_file call in the model. We add the :class option to include the model_name as a folder.

has_attached_file :image,
  :styles => {:thumb => '120x120>', :large => '640x480>' },
  :default_style => :thumb,
  :url => "/system/class/attachment/id/style/basename.extension",
  :path => ":rails_root/public/system/:class/:attachment/:id/:style/:basename.:extension"

NOTE: Thanks to commenter Steve Bartz for pointing out that current versions of Paperclip have the default path set to ":rails_root/public:url". Because of this, you can leave out the :path option.

So, now our folder structure for the original example would be public/system/blog_posts/images and public/system/blog_posts/files. Much better!

Just make sure you put the whole path in the :path option using the :rails_root variable.

Tagged: rubyrailspapercliptutorial

Emulating RJS with Merb/JQuery/Haml

Dec 21, 2008  -  Comments

Ok, Merb is great. Rails is equally as great. RJS is great. JQuery and Haml are just outstanding. But I found that Merb not doing RJS-like behavior a little daunting in the current application that I'm working on. Of course, Merb does javascript, and you can send Ajax requests, but it's not quite as easy to accomplish RJS-type interactions with the current page. I did quite a bit of googling, and all I ever came up with was a very interesting presentation from Yehuda Katz about using JQuery with Merb. It didn't completely answer my question about getting RJS functionality in a Merb app. So, I set about trying to actually figure something out on my own!

Let's say I have an action called mark_as_complete that I can call for several items on a page, have it make a database call, then update the item without having to leave the page. The first step is to make the form for each item submit via AJAX instead of a regular HTTP POST request. Thanks to Ryan Bates' RailsCast on JQuery for help on that. All I have to do is give the form a class of remote, then this function will cause it to be submitted via AJAX (as well as any form on the site with the same class name):

// function to send the jQuery form object via AJAX
jQuery.fn.submitWithAjax = function() {
  this.submit(function() {
    // be sure to add the '/4/.js' part so that it knows this is format:js
    $.post(this.action + '/4/.js', $(this).serialize(), null, "script");
    return false;
  })
  return this;
};

$(document).ready(function() {
  // once the page has completely loaded,
  // make forms with the class "remote" submit via AJAX
  $("form.remote").submitWithAjax();
});

Now that's done, I need to figure out how to get the RJS stuff to fire correctly. I've already set the format of the request as .js, so the controller knows it's an AJAX request. With Merb, you have to add the provides :js line to your action so that it knows to accept js requests.

def mark_as_complete(id)
  provides :js
  # do work, son
  # set instance variable to indicate success/failure
  if stuff_done
    @complete = true
  end
  render
end

Now for the funky part. Getting my Haml template to update the page. Well, if you just name a view mark_as_complete.js.haml, then the application will use it. The hard part was figuring out what to do in the file. I could call javascript in the file and get that to work (alert(), for example), but the following bit of code did NOT work:

- if @complete
  $("#div_name_#{@record.id}").html('complete');
  $("#notes_#{@record.id}").replaceWith('

#{@record.notes}

'); - else alert("An error occurred processing your request.");

Nothing happened and I couldn't figure out why. The javascript was being run, but the ruby code inside it wasn't being evaluated. Turns out it's because I'm using Haml. I suspect that if I were using erb, then I could surround my ruby code with output blocks(<%= %>) and it would work(though I haven't tested it).

After experimenting with all sorts of variations on the above code, I finally decided to try and put the javascript code into Haml ruby output blocks(so the ruby code would be evaluated). Low and behold, the following actually worked, despite being ugly and less readable.

- if @complete
  = "$('#div_name_#{@record.id}').html('complete');"
  = "$('#notes_#{@record.id}').replaceWith('

#{@record.notes}

');" - else alert("An error occurred processing your request.");

Notice that the alert() call isn't in an output block because it doesn't have any ruby code that needs to be evaluated.

This solution worked for me, but I'm sure there is a better way to accomplish this. If anyone has a better approach, please leave it in the comments.

Tagged: rubyrjsmerbjqueryhamltutorial

Helpful Helpers

Nov 30, 2008  -  Comments

Here is a list of helper methods that we use a lot at Plexus to make our jobs easier. A few have been referenced in previous posts, but I thought I'd list them all here in one place.

Strip text for pretty URLs

You can use this method to replace all non-alphanumeric characters to dashes in a title or name when you're including them in the URL.

This is the route I use for my posts on this blog:

m.post_details ':year/:month/:day/:title', :action => "by_date",
  :requirements => { :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/},
  :day => nil, :month => nil, :title => nil

And here is the helper to strip out characters in the title when creating the URL:

# Replace all non-alphanumeric characters with dashes
def strip_chars(string='')
  string.gsub(/\s+/,'-').gsub(/[^a-z0-9\-]+/i, '')
end

You can call the helper when you generate the route.

post_details_path(:year => @post.date.year, :month => @post.date.month, :day => @post.date.day, :title => strip_chars(@post.title))

Generate a random password of n length

I've actually got a whole other post for just this helper, but decided to include it in this list.

def generate_password(length=6)
  chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789'
  password = ''
  length.times { |i| password << chars[rand(chars.length)] }
  password
end

It will generate a 6 digit password by default, but you can specify the desired length.

generate_password
=> "Q6Kfze"

generate_password(10)
=> "ZSJcmtRH5q"

Built-in rails helpers

These following helpers are built into rails, but you may not know about them.

Array#to_sentence

["this", "that", "the other"].to_sentence
=> "this, that, and the other"

number helpers

number_to_currency(123456789)
=> "$123,456,789.00"

number_to_human_size(123456789)
=> "117.7 MB"

number_to_phone(1234567890, :area_code => true)
=> "(123) 456-7890"

There are a ton of string inflector helpers

"under_scored".dasherize
=> "under-scored"

"names_and_titles".humanize
=> "Names and titles"

"lower case title".titleize
=> "Lower Case Title"

Tagged: rubyrailshelpers