Sharing Code Between ActiveRecord Models

January 29th, 2008 by Trevor

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!

January 24th, 2008 by Trevor

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: eldoradoapp.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

January 7th, 2008 by Trevor

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'].singularize.classify.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...

 

Configuring Cookie-Based Sessions in Rails 2.0

December 27th, 2007 by Trevor

As of Changeset 6184 and the release of Rails 2.0, the default session store for Rails apps is cookie-based.

[This] means sessions are no longer stored on the file system or in the database, but kept by the client in a hashed form that can't be forged. This makes it not only a lot faster than traditional session stores, but also makes it zero maintenance. There's no cron job needed to clear out the sessions and your server won't crash because you forgot and suddenly had 500K files in tmp/session.

Configuring your application to use this speedy new session store is easy. Adding the following to your config/environment.rb file would do the trick:

config.action_controller.session = {
  :session_key => '_my_app_session',
  :secret      => 'some_really_long_and_hashed_key'
}

But...

I don't like it.

Especially when you're dealing with open-source projects, putting what amounts to installation-specific passwords here doesn't seem appropriate. In the case of my open-source project, El Dorado, I'd like to be able to make changes to environment.rb without troubling the user. Ideally, I think all passwords should be set from a single location. Luckily, it's easy to push this configuration into the already available config/database.yml.

Here's how.

Add the following to config/environment.rb:

require 'yaml'
db = YAML.load_file('config/database.yml')
config.action_controller.session = {
  :session_key => db[RAILS_ENV]['session_key'],
  :secret      => db[RAILS_ENV]['secret']
}

And then you can set everything up in one place: config/database.yml:

development:
  adapter: mysql
  database: eldorado_development
  username: root
  password:
  host: localhost
  session_key: eldorado_development
  secret: YrDOFOmYJyFg2tTZykCbZjWYQUbKBt

test:
  adapter: mysql
  database: eldorado_test
  username: root
  password:
  host: localhost
  session_key: eldorado_test
  secret: Pl8qJNFc8mo1yt1xtHOmfUGHOPEutu

production:
  adapter: mysql
  database: eldorado_production
  username: root
  password:
  host: localhost
  session_key: eldorado_production
  secret:

This seems more... natural. Don't you think?

Anyway, using YAML files for app configuration is the way of the future.

 

Random Records in Rails

December 4th, 2007 by Trevor

There are a number of different ways to retrieve random items from a database in Rails, most of which have been discussed at length on the Rails wiki. According to this article, there are 3 preferred methods:

  1. 1. Select a Record by Random Offset
  2. 2. Randomize with the Database
  3. 3. Randomize with Ruby

For an in-depth look into these options, you can peruse this discussion page, which details some of the pros and cons of different strategies for randomization.

After trying out a few of the techniques that abound in this area, I stumbled across an article from Jamis Buck, where he discusses a RESTful way to approach the creation of custom finders. Although randomization isn't the focus of the article, he does provide a bit of guidance in that regard. His strategy uses Ruby for the randomization, and employs 2 light-weight queries. The first query gathers a list of valid ids. The second simply selects a single item using that (randomized) id.

So, I did a bit of cargo-culting and repurposing to achieve an efficient, database agnostic way to retrieve random items from a database using ActiveRecord. Simply add the following to the model from which you'd like to be able to pull random records:

class Widget < ActiveRecord::Base
 
  # ...
 
  def self.random
    ids = connection.select_all("SELECT id FROM widgets")
    find(ids[rand(ids.length)]["id"].to_i) unless ids.blank?
  end
 
end

Then, you can use the following bit of code in a controller like so:

class SomeController < ApplicationController
 
  # ...
 
  def some_action
    @widget = Widget.random
  end
 
end

And you've got a small and efficient "random finder" for use throughout your app. Lovely.

 

Graceful 404s in Rails 2.0

October 8th, 2007 by Trevor

With the upcoming Rails 2.0: Preview Release starting to get some attention, I thought I'd take a moment to play with some of the new features. One of my favorite additions is the new exception handling stuff. It works just like a before_filter, so you'll pick it up straight away.

Action Pack: Exception handling:

Lots of common exceptions would do better to be rescued at a shared level rather than per action. This has always been possible by overwriting rescue_action_in_public, but then you had to roll out your own case statement and call super. Bah. So now we have a class level macro called rescue_from, which you can use to declaratively point certain exceptions to a given action.

The following is quick example you can use to catch 404 Record Not Found errors. It will catch all 404s on your site and display a nice message, instead of an ugly white page of death:

file-not-found-1.jpeg

Simply dip into the ApplicationController and add the following code:

class ApplicationController < ActionController::Base
 
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
 
  # ...
 
  def record_not_found
    flash[:notice] = "Sorry, the page you requested was not found."
    redirect_to root_path
  end
 
  # ...
 
end

...to display a nice message within your app:

file-not-found-2.jpeg

...and there you have it: Easy as Pie(tm) Record Not Found exception handling.

P.S.

This also takes advantage of my favorite tiny addition to Rails, map.root. I didn't see this mentioned in the release announcement, but it's covered in the video of the Railsconf Europe '07 video.

Instead of this:

map.home '', :controller => 'home'

...you can now do this:

map.root :controller => 'home'

It's not a big change, but it's just... nice, isn't it?

 

Administration Tips 01: SSH tweaks on your new remote server

September 1st, 2007 by Timothy O'Connell

Hola, gang: my name is Tim and I'm going to be contributing to the AE blog periodically. I'll be contributing tips and advice and I'll be writing from an admin's perspective.

A bit about me: I'm a system administrator and I work for a small, freelance IT company. Ours is a Debian shop; we've got a handful of programmers, two admins and we do custom apps in addition to other IT support work for clients. I'm the junior guy at my place and, basically, I'm the understudy to one of greatest admins who ever jocked a console.

As this is my first post, I am going to keep it short and simple. I assume only that you have a basic understanding of bash. In fact, I think that's the only assumption that I'm going to make from here on out.

Now that virtual servers are all the rage and remote hosting is unignorably cheap, lots of cats with very limited prior experience are finding themselves playing administrator on machines responsible for serving important apps, sites, etc.

One of the first, easiest and best things you can do to improve the security of your remote box is lock SSH down tight. Assuming you've already got root on your remote box and the "ssh" package installed, what you're going to want to do next is make your personal user an sudoer with full access:

# aptitude install sudo
...
# echo "toconnell ALL=(ALL) ALL" >> /etc/sudoers

Now edit /etc/ssh/sshd_config with your favorite editor to prevent remote users from logging on to your box as root and change the SSH server's default port:

Port 222
PermitRootLogin no

Now restart your ssh server:

# /etc/init.d/ssh restart

The big idea here is that no one can log in as root and the hundreds of bots/zombies that will be constantly pinging your IP address on port 22 will be rejected summarily (and not fill up your logs with half-assed attempts to root your box); to them it will appear as though you are not even running an SSH server.

And that's about it: you modified three lines in two conf flies and hardened your Debian server up substantially. There are some trade-offs, of course. In order to root your box remotely, you've got to specify the port on which you want to connect and then su to root:

$ ssh -p 222 toconnell@remoteserver.com
toconnell@remoteserver.com's password:
toconnell@remoteserver:~$ sudo su
Password:
remoteserver:/home/toconnell#

In my estimation, the slight inconvenience is worth it. If you don't believe me, take a look at your logs and see who is knocking on your door, how often and in what manner.

 

Capistrano2 Recipe for Slicehost

August 4th, 2007 by Trevor

After moving from DreamHost over to Media Temple a few months ago, I ran into some issues with the Rails Containers and have since moved on to Slicehost. I'm extremely happy over there, and I'd recommend them to anyone not afraid of the command line.

Since my Capistrano Recipe for Media Temple still gets a fair amount of traffic, I figured I might as well share my new Capistrano 2 Recipe for Slicehost - specifically Debian Etch.

set :application, "eldorado"
set :repository,  "http://eldorado.googlecode.com/svn/trunk/"
set :deploy_to, "/home/eldorado"
set :deploy_via, :export
set :user, 'root'
 
ssh_options[:port] = 22
 
role :app, "000.00.00.000"
role :web, "000.00.00.000"
role :db,  "000.00.00.000", :primary => true
 
after 'deploy:update_code', 'deploy:upload_database_yml'
after 'deploy:update_code', 'deploy:create_symlinks'
 
namespace :deploy do
  task :restart do
    run "/var/lib/gems/1.8/bin/mongrel_rails stop -P #{shared_path}/log/mongrel.8000.pid"
    run "/var/lib/gems/1.8/bin/mongrel_rails start -d -e production -p 8000 -P log/mongrel.8000.pid -c #{release_path}"
  end
end
 
namespace :deploy do
  task :upload_database_yml do
    put(File.read('config/database.yml'), "#{release_path}/config/database.yml", :mode => 0444)
  end
end
 
namespace :deploy do
  task :create_symlinks do
    %w{avatars files headers}.each do |share|
      run "rm -rf #{release_path}/public/#{share}"
      run "mkdir -p #{shared_path}/system/#{share}"
      run "ln -nfs #{shared_path}/system/#{share} #{release_path}/public/#{share}"
    end
  end
end

While we're at it, here's the accompanying Apache config:

ServerName 127.0.0.1
NameVirtualHost *:80



	ServerName example.com
	ServerAlias www.example.com
	DocumentRoot /var/www/eldorado

	
		Options FollowSymLinks
		AllowOverride None
		Order allow,deny
		Allow from all
	

	RewriteEngine On

		BalancerMember http://127.0.0.1:8000
	

		Order deny,allow
		Allow from .example.com
	

	# Check for maintenance file and redirect all requests
	RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
	RewriteCond %{SCRIPT_FILENAME} !maintenance.html
	RewriteRule ^.*$ /system/maintenance.html [L]

	# Rewrite index to check for static
	RewriteRule ^/$ /index.html [QSA] 

	# Rewrite to check for Rails cached page
	RewriteRule ^([^.]+)$ $1.html [QSA]

	# Redirect all non-static requests to cluster
	RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
	RewriteRule ^/(.*)$ balancer://mongrel_cluster%{REQUEST_URI} [P,QSA,L]

	ErrorLog /home/eldorado/shared/log/error_log
	CustomLog /home/eldorado/shared/log/access_log combined

	RewriteLog /home/eldorado/shared/log/rewrite_log
	RewriteLogLevel 9

Of course, this would require creating a symlink in /var/www/example to your installation's "current" directory, as set up by Capistrano:

ln -s /home/eldorado/current/public /var/www/example

This work was done on behalf of El Dorado, an open-source Rails forum I've been hacking away at for the last few months. I'll post more about that soon, but you can start playing with the beta version now.

There's also a live version of the app, which is (hopefully) up and running over at NewAthens.org. NewAthens was formerly running on a hodgepodge of PunBB, WordPress, PmWiki, Advanced Poll, and Coppermine. El Dorado represents the beginning of my attempt to create a full stack community site in Rails.

The existing database from the PunBB forum was successfully imported on Saturday using some handy Rake tasks I created, as were all of the user-uploaded files, etc. The resulting database has around 35,000 rows, and it's been up and running without any major problems for a week now. Huzzah!

 

The Perfect Timestamp

July 29th, 2007 by Trevor

holy-grail.gif

Almost every web application deals with and displays timestamps. Searching for the perfect one can be a bit like searching for the Holy Grail. How can something so small, so seemingly innocuous, cause so much heartache; so much pain? I think the problem is that too many choices leads to suffering when it comes to software development.

A timestamp needs to be short and sweet, easy to read, and yet highly informative. There's a lot of information that you might include, and options too numerous to mention for displaying the various bits of data. But even strftime doesn't cover all of the bases. For example, AM and PM always come out in ALL CAPS. What's the deal with that? [edit] Code updated to use the Date strftime method, which is much better for some odd reason. [/edit]

Well, I'm all for less choice and smart defaults, but I don't think even the date/time helpers in Rails get it quite right. Hell, I don't blame them - I think this has got to be one of the most perplexing and difficult problems in software development, second only to coming up with good names for stuff.

I'm sure I must have gone through at least 50 iterations, looking for the perfect solution. Now, I'm pleased to say that I think I've finally found it.

Sat, 28 Jul 2007, 3:15pm

If I had to pick one timestamp to take with me on a deserted island, this would be the one. It's short and sweet, but it still packs a lot of punch.

Here's the helper I made to accomplish this spectacular feat:

def time_stamp(time)
  time.to_datetime.strftime("%a, %d %b %Y, %l:%M%P").squeeze(' ')
end

Easy as pie. But...

There's still something missing. What about that "2 hours ago" kind of timestamp. You know, something like the distance_of_time_in_words helper? I think people like to see if some item was created "2 hours ago," sure. But at a certain point, saying "22 days" ago just doesn't make sense.

So, we need to find a middle ground. Ideally, a time_ago helper that automatically adjusts to display a proper timestamp after a certain amount of time...

def time_ago_or_time_stamp(from_time, to_time = Time.now, include_seconds = true, detail = false)
  from_time = from_time.to_time if from_time.respond_to?(:to_time)
  to_time = to_time.to_time if to_time.respond_to?(:to_time)
  distance_in_minutes = (((to_time - from_time).abs)/60).round
  distance_in_seconds = ((to_time - from_time).abs).round
  case distance_in_minutes
    when 0..1           then time = (distance_in_seconds < 60) ? "#{distance_in_seconds} ago" : '1 minute ago'
    when 2..59          then time = "#{distance_in_minutes} minutes ago"
    when 60..90         then time = "1 hour ago"
    when 90..1440       then time = "#{(distance_in_minutes.to_f / 60.0).round} hours ago"
    when 1440..2160     then time = '1 day ago' # 1-1.5 days
    when 2160..2880     then time = "#{(distance_in_minutes.to_f / 1440.0).round} days ago" # 1.5-2 days
    else time = from_time.strftime("%a, %d %b %Y")
  end
  return time_stamp(from_time) if (detail && distance_in_minutes > 2880)
  return time
end

That little beauty also has the ability to leave out the hours/minutes part of the timestamp, for those cases where you just don't have space for that kind of detail. It's also a fair bit less verbose than the built-in Rails helper, which has always rubbed me the wrong way.

All of this work with timestamps was done on behalf of El Dorado, an open-source Rails forum I've been hacking away at for the last few months. I'll post more about that soon, but you can start playing with the beta version now. The relevant helpers around timestamps can be found in the application_helper.

Update: Alex over at Rails Forum posted a couple of helpful suggestions about this. Thanks!

 

Mass Edit Pages for Wordpress 2.1.x and up

May 24th, 2007 by Trevor

I haven't bothered to upgrade my Wordpress installation away from the 2.0.x series yet, primarily because Mark Jaquith & Co. have committed to providing security updates to the 2.0 line through 2010. However, Jan and Gunnar were nice enough to update my Mass Edit Pages plugin to work with Wordpress versions 2.1.x and the new 2.2 series.

While they were at it, they made the plugin I18n compatible, which is great for people who don't speak English. They even included a version that comes with the full German translation. Thanks, guys!

Update: Gunnar provided a small update for the 2.1.x version of this plugin today, which fixes a problem that occasionally popped up involving updating the Wordpress cache.

Download the plugin »