Recent Projects

Sharing Code Between ActiveRecord Models

I was frustrated with the amount of duplicated code sprinkled in my models that are using the attachment_fu plugin to deal with file uploads. It’s not like I need to write a plugin or anything fancy – I just want to be a little more DRY.

So, how does one simply share code between ActiveRecord models? It’s actually quite easy.

First, drop some code into a module in a .rb file in your lib directory. For example:

# lib/attachment_fu_extensions.rb

module AttachmentFuExtensions

  # include some standard attachment_fu stuff
  def self.included(klass)
    klass.send :validates_as_attachment
    klass.send :validates_uniqueness_of, :filename
    klass.send :validates_presence_of, :user_id
    klass.send :attr_protected, :id, :parent_id, :user_id, :created_at, :updated_at
  end

  # upload into a single directory (e.g. public/uploads) instead of subdirectories based on the id
  def full_filename(thumbnail = nil)
    file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
    File.join(RAILS_ROOT, file_system_path, thumbnail_name_for(thumbnail))
  end

  # prevent users from uploading index files that would be served instead of the index template/action
  def validate
    errors.add("filename", "is invalid") if filename? && %w(index.html index.htm).include?(filename.downcase)
  end

end

That code will automatically be loaded when your application starts, provided that the filename and module follow the standard Rails naming conventions.

Then, simply include the relevant module in the relevant models. For example:

# app/models/upload.rb

class Upload < ActiveRecord::Base

  has_attachment :storage => :file_system, :path_prefix => 'public/uploads', :max_size => 10.megabytes
  include AttachmentFuExtensions

  # ...

end

That’s it. Extending ActiveRecord models and DRYing out your app is pretty easy, eh?

Update: Walter pointed out that I should put modules like this into lib/ instead of config/initializers/, so I’ve updated this post to reflect that. I’m not sure why that didn’t occur to me. He also told me about the trick for sharing calls to validates_as_attachment and so forth – I didn’t even know that was possible!

Welcome to El Dorado!

eldorado

I’m pleased to announce the first public release of El Dorado: a full-stack community web application written in Ruby/Rails. This is a stable beta version of the app, which is being released in anticipation of the 1.0 release scheduled for this spring.

The app features a forum, community event calendar, shared file storage, and a randomized header image gallery. The forum is somewhat modeled on PunBB (one of the more popular PHP forums) and can import from an existing site using PunBB.

The app has been used in production since late July ’07. The setup/administration process still leave something to be desired, but the end-user functionality is quite well tested.

HomeForumTopicsFilesEvents

You can get the download or more information on the El Dorado homepage, or check out the demo, testing, and support site here: eldorado.heroku.com. Also, consider subscribing to the almost effortless RSS feed to stay informed about upcoming releases and the occasional Ruby/Rails-related post.

Enjoy!

Simplifying and sharing code with Rails conventions

One of the benefits of convention over configuration in Rails is that you can make assumptions about a lot of stuff. A good example of this is the user_id column found in most Rails database tables.

If you see a user_id column in a schema, you can typically infer that it represents the unique identifier (id) of the user that created that record. This leads to all kinds of interesting opportunities for code simplification.

I’ll take you through a basic example found in my open-source app, El Dorado to demonstrate. This example will show you how to share code related to authentication throughout your app. We’ll start by looking at the user, topic, and post database objects defined in schema.rb:

create_table "users", :force => true do |t|
  t.string   "login"
  t.boolean  "admin", :default => false
  # ...
end

create_table "topics", :force => true do |t|
  t.integer  "user_id"
  t.string   "title"
  # ...
end

create_table "posts", :force => true do |t|
  t.integer  "user_id"
  t.text     "body"
  # ...
end

According to Rails conventions, the user_id field in the topics and posts tables should reference the id field in the users table. (The id field is assumed and not mentioned in the schema.rb file.) This convention is what allows you to do things like:

@user = User.find(1)
@user.topics # the topics the user has created
@user.posts # the posts the user has created

This also lets you share code amongst all of the controllers and views in an application. Let’s use a basic concept for authentication: checking for permission to edit an item. Using some of the conventions introduced in the restful_authentication plugin, we’ll be able to add some basic permission checking that will work for any model that has a user_id field, and for users that have an id field. We’ll assume that a user has permission to edit an item if (a) they’re an administrator, or (b) they’ve created the item they’re trying to edit.

We’ll start by adding the following to application.rb:

class ApplicationController < ActionController::Base

  helper_method :current_user, :logged_in?, :admin?, :can_edit?

  def current_user
    @current_user ||= ((session[:user_id] && User.find_by_id(session[:user_id])) || 0)
  end

  def logged_in?()
    current_user != 0
  end

  def admin?()
    logged_in? && (current_user.admin == true)
  end

  def can_edit?(current_item)
    return false unless logged_in?
    if request.path_parameters['controller'] == "users"
      return current_user.admin? || (current_user == current_item)
    else
      return current_user.admin? || (current_user.id == current_item.user_id)
    end
  end

  def can_edit
    redirect_to root_path and return false unless logged_in?
    klass = request.path_parameters['controller'].camelize.singularize.constantize
    @item = klass.find(params[:id])
    if request.path_parameters['controller'] == "users"
      redirect_to root_path and return false unless admin? || (current_user == @item)
    else
      redirect_to root_path and return false unless admin? || (current_user == @item.user)
    end
  end

  # ...

end

The current_user bit assumes that you're setting the session[:user_id] value to the currently logged-in user's id. (If you're unsure how to accomplish this and/or aren't doing it already, you should give the restful_authentication plugin a gander.) The logged_in? helper will return true if the user is logged in, and the admin? helper will return true if the logged-in user has their admin attribute set to true.

The can_edit? helper and the can_edit action are where the real action is. We'll start with the can_edit action, which allows you to do the following in any controller in your application:

class TopicsController < ApplicationController

  before_filter :can_edit, :only => [:edit, :update, :destroy]

end

# ...

class PostsController < ApplicationController 

  before_filter :can_edit, :only => [:edit, :update, :destroy]

end

# ...

class UsersController < ApplicationController

  before_filter :can_edit, :only => [:edit, :update, :destroy]

end

The can_edit action is accessible to any controller, and it can be used to check the editing permissions for any item without any additional code overhead. If the user is logged in and is an administrator, they'll be allowed to proceed. Otherwise, the action will check to see if the user is trying to edit an item that they created, or their own user account. If so, they'll be allowed to proceed; if not, they'll be redirected to the root_path. This, of course, requires the defining of a root_path in your routes.rb file:

map.root :controller => 'home'

That's pretty nice, and there's also the can_edit? helper that can also be shared across all views. Here's a couple of examples. The first would be useful in, say, the user/show view. The second, perhaps in posts/show...?

<%= link_to 'Edit User', edit_user_path(@user) if can_edit?(@user) %>
<%= link_to 'Edit Post', edit_post_path(@post) if can_edit?(@post) %>

And there you have a great example of the benefits of convention in Rails app. You can do a lot of cool stuff with very little code. Plus, DRY code is always nice. Of course, you can dig further into the whole kit and kaboodle behind this by checking out the El Dorado source...