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

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 »

 

Working with attachment_fu

March 25th, 2007 by Trevor

Now that the fantastically easy-to-use file-managing plugin for Rails attachment_fu Rails plugin is production-ready, Mike Clark has published a great tutorial on how to use it.

After following Mike's instructions, I started poking around in my app and discovered that attachment_fu saves uploads in subdirectories based upon the file id. That makes sense in a lot of ways, but it's not compatible with my app's current structure. What I need is to save the uploaded files in a single directory. So, if I have a Model that has_attachment called "uploads", I'd like all of the uploaded files to be stored in the "uploads" directory - sans subdirectories.

Rick offers a hint at a solution, but for those of us who aren't quite so technically-inclined, I figured a quick code snippet might help. Here's my /app/models/upload.rb:

class Upload < ActiveRecord::Base
 
  has_attachment :storage => :file_system, :path_prefix => 'public/uploads'
  validates_as_attachment
  validates_uniqueness_of :filename
 
  # Update: Consider using the new partition option instead http://github.com/technoweenie/attachment_fu/commit/8a1e64
  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
 
end

That's all you need to get your uploads uploaded to public/uploads :) We're overwriting the full_filename bit to remove one tiny little bit that specifies the subdirectory. We're also using "validates_uniqueness_of" to ensure that the filename is unique and avoid overwriting existing files with the same name.

Now, that's only half the battle if you're working with an app in production. I'm using Capistrano, as I'm sure you are. The way Capistrano works, anything that gets uploaded into your production app is going to be "lost" when you deploy a new release. In reality, your files are left alone, it's just that they're stuck in an old "release" directory and are basically useless there.

So, how do you upload files to your production app? First, you'll need a directory for your uploads in your shared/system folder. This shared/system folder is something that Capistrano sets up to hold files that you'll want to keep as you deploy new releases. It's the perfect place to store uploaded content. The result will look something like this:

appname => shared => system => uploads

Second, you'll need a symlink that points to this shared directory. This symlink will allow your app to pretend as though it's working within public/uploads, when it's actually saving everything to shared/system/uploads. That way, everything will work in the same way between your development and production environments, and you won't lose uploaded files in your production app.

Luckily, this can all be accomplished by adding a few lines to your deploy.rb file. Here's the snippet:

task :after_update_code, :roles => :app do
  %w{uploads whatever somethingelse}.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

Unfortunately, I can't remember where I got this tip from, but it's a great bit of DRY code. Basically, this will allow you to add as many different upload types as you like to your app, with very little code. If you're only using "uploads" then you can delete the "whatever" and "somethingelse" parts, but I figured I should leave them in there to illustrate the point.

So, that's just about it. One last thing is to ignore the files that you upload into your development app. There's no point in putting those into version control, and it's easy to ignore them with Subversion. Here's the code snippet that I needed to run from my app's "trunk" directory:

svn propset svn:ignore "*" public/uploads/

That'll ignore all files and subfolders of your public/uploads directory.

I think that's all there is to it! Thanks very much to Mike Clark for his excellent tutorial, and (of course) to Rick Olson for his totally awesome plugin!

Update: I'm going to follow up on this when I have more information, but it seems that there's a problem where Apache automatically appends trailing slashes in cases where you've got a directory/symlink in your public folder that matches a named route. This can cause strange routing issues that are difficult to track down, but there appears to be an easy fix. Adding this:

DirectorySlash Off

...to your .htaccess file will disable Apache's addition of the trailing slash, and you should be good to go.

 

Active Record and the IN clause

March 19th, 2007 by Trevor

So here's the setup:

Site => Categories => Forums => Topics => Posts

That's a pretty standard setup for a web-based forum, where Topics are in a particular Forum and those Forums are, in turn, organized by Category. Retrieving the Topics in a particular Forum is easy, since I have a "forum_id" column in my "topics" table:

@topics = Topic.find(:all, :conditions => ["forum_id = ?", @forum.id])

But what if I want to retrieve the Topics in a particular Category? That's not quite as easy, since there's no "category_id" column in my "topics" table. What to do? The "WHERE IN" clause is what you're supposed to use here, I think. That's when you make a query something like this:

@topics = Topic.find(:all, :conditions => ["forum_id in ?", @forums])

This might read something like "find all topics where the forum_id is in my @forums object". But (of course) that won't work. What to do? Hack together a string of forum_ids that's worthy of passing into this query? There's got to be a better way.

When in doubt, I always turn to the fabulous open-source work of one Rick Olson, because searching around the Rails API or plain old Google isn't nearly as informative as seeing real live code examples. And when it comes to really nice real live code examples, it doesn't get any better than techno-weenie.

So, what's the solution?

@category = Category.find(params[:id])
@forums = Forum.find_all_by_category_id(@category.id)
@topics = Topic.find(:all, :conditions => ["forum_id in (?)", @forums.collect(&:id)])

Easy as pie.

 

Redirect old URLs in Rails

February 15th, 2007 by Trevor

I'm in the process of porting over a PHP site to Rails, but I don't want to break any old links. The way things work now, the URL to view a particular topic would be something along these lines:

http://example.com/view.php?id=1

I'd like these to redirect the user to the appropriate topic, and I'll be maintaining the id value when moving everything over to the new database. So, it turns out that catching all of these PHP-style links and forwarding them over to the new system is pretty easy.

I started by adding a catchall bit in my routes.rb like so:

map.catch_all "*path", :controller => "topics",  :action => "unknown_request"

Then, I threw together an unknown_request action in my topics_controller.rb:

def unknown_request
  if request.request_uri.include?('view.php')
    redirect_to topic_path(:id => params[:id])
  else
    redirect_to topics_path # or some other path
  end
end

Easy as pie! Plus, I can add elsif statements ad nauseam if I need to catch other deprecated URLs.

Hope this helps somebody save some time out there on the Internets. I found a couple other posts about this, but this technique seems pretty straightforward. Feel free to comment if you know of a better way!

 

Capistrano on Media Temple

January 28th, 2007 by Trevor

I recently moved most of the sites I'm maintaining over to Media Temple's Grid Server hosting service because I was having no luck getting Rails apps deployed on Dreamhost. I gather that people much brighter than me have gotten Rails apps running on Dreamhost without any difficulties, but I just wasn't able to get it working myself and was looking to move hosts in any case.

I chose Media Temple in large part because of their focus on Rails Containers and because of the tools they've built that make it easy to get up and running. It doesn't hurt that they've got pretty reasonable prices, too.

One of the main things that got me interested in Rails is that its "opinionated" nature starts you off on the right foot. Rails apps have development, testing, and production environments from the start, which makes it very easy to make changes to live apps without worrying about screwing things up. Capistrano makes the deployment part of creating and maintaining Rails apps extremely easy. The only thing is that you've got to create a "deployment recipe" for use on your web host's servers.

Here is the Capistrano Deployment Recipe (deploy.rb) that I put together for Media Temple. If you've followed the instructions in the Server Guide for how to prepare your (gs) for Rails hosting, just set the proper passwords and stuff in the first few lines and you should be good to go. Enjoy!

 
require 'mt-capistrano'
 
set :site,         "0000"
set :application,  "appname"
set :webpath,      "app.com"
set :domain,       "primarydomain.com"
set :user,         "serveradmin%primarydomain.com"
set :password,     "password"
 
ssh_options[:username] = 'serveradmin%primarydomain.com'
 
set :repository, "svn+ssh://#{user}@#{domain}/home/#{site}/data/svn/#{application}/trunk"
set :deploy_to,  "/home/#{site}/containers/rails/#{application}"
 
set :checkout, "export"
 
role :web, "#{domain}"
role :app, "#{domain}"
role :db,  "#{domain}", :primary => true
 
task :after_update_code, :roles => :app do
  put(File.read('config/database.yml'), "#{release_path}/config/database.yml", :mode => 0444)
end
 
task :restart, :roles => :app do
  run "mtr restart #{application} -u #{user} -p #{password}"
  run "mtr generate_htaccess #{application} -u #{user} -p #{password}"
  migrate
end
 

I originally posted this code on the Rails Weenie forums, but I thought it might be good to post it here so that it doesn't get lost. This blog post also provided some serious help when I was setting things up, and I think it's worth a read as well.

If you're brand new to Capistrano, you'll have to first install the gems for "capistrano" and Media Temple's "mt-capistrano" like you would with any other gem. You'll also want to have Subversion set up for your code repository. If you don't have SSH keys set up, I think you can add ssh_options[:password] to the recipe instead.