El Dorado 0.9.2 (Group Chat Edition)

May 5th, 2008 by Trevor

I'm pleased to announce the release of El Dorado version 0.9.2 - the "group chat" edition. You can download it right away, or check out the demo, testing, and support site here:

http://eldoradoapp.com/

Since we last spoke at the end of January, there's been a goodly amount of activity with El Dorado. The highlight, of course, is the new group chat feature:

eldo-chat-2

This is a very simple and lightweight implementation that's perfect for small groups. It works solely with the tools provided by Rails and doesn't add anything to the overall requirements for running El Dorado. I've been using it with my group of friends for a few weeks now, and it's been working great. I'm sure that I'll have some more features to announce with the next release, but my favorite little touch so far is the "who's in the chat room and when was the chat last active" indicator on the home page:

eldo-chat-1

In addition to the addition of group chat, there have been a number of improvements to other parts of the app and quite a bit of back-end optimization/refactoring. Here's a quick list of the new features:

  • Make entire site private with a simple admin setting (great for small companies)
  • View all posts across the entire forum in reverse chronological order
  • View all posts by individual user
  • Upload new files quickly and easily via URL
  • Ability to "sticky" and "lock" forum threads
  • Move threads to different forums, and forums to different categories
  • Allow users to stay logged in between different browsers/machines
  • New calendar interface for setting the date/time of an event
  • Admin interface for viewing/editing/creating user rankings
  • Admin interface to grant/revoke admin privileges
  • Various BBCode additions (Flickr video, Slideshare, FunnyOrDie, etc) and some bug fixes

Of course, you can follow along with all of the activity in El Dorado on the commit log at github. You see, being the lemming that I am, I've followed Rails to github and Lighthouse already. I haven't had time to settle into Lighthouse just yet, but I've been having a blast with git and github.

In fact, I gave a presentation at Chicago Ruby and spent a fair amount of time talking about making the move to git. Definitely flip through the PDF to learn all about the origin of El Dorado (where the name came from, etc) but take my word for it about one thing: git/github is the way to go:

eldo-chicagoruby

If you believe the PDF, then the upcoming of releases should go something like this:

  • Chat
  • Localization (see note below)
  • Blog
  • Themes
  • Photos
  • Wiki
  • Polls
  • Link Sharing
  • Mobile Messaging

Thanks to git/github, jxl has been working on a localized version of El Dorado using Gibberish, and he's already completed a Dutch translation. I'll be working with him to pull his changes into my repo for the next release. If you're interested in translating El Dorado into another language, please do get in touch!

I think that about covers it... so... Welcome to El Dorado v.0.9.2!

Learn more and/or download El Dorado here: http://almosteffortless.com/eldorado/

 

Automatically Creating, Loading, and Migrating your Database (with magic!)

April 10th, 2008 by Trevor

So, let's say you're working on this open source Rails app, and you're doing your best to keep the requirements light and the installation as easy as possible. You'd love to have that Famous 5-Minute Install that WordPress has going for it, and you start to wonder...

Why should I even have to worry about the database?

Why can't my app take care of itself? Why can't it automatically create and maintain the database for me?

Well friend, I just wrapped up a little initializer that will automatically create the db, load the schema, and/or migrate as necessary. It all seems fine so far, but I still have this funny feeling that I'm doing something wrong...

 
/config/initializers/db_create_load_or_migrate.rb
 

I dunno... Have I gone too far this time?

 
unless defined?(Rake)
  load "#{RAILS_ROOT}/Rakefile"
  begin
    current_version = ActiveRecord::Migrator.current_version
    highest_version = Dir.glob("#{RAILS_ROOT}/db/migrate/*.rb").map { |f| f.match(/(\d+)_\w*\.rb$/) ? $1.to_i : 0 }.max
    Rake::Task["db:migrate"].invoke if current_version != highest_version
  rescue
    Rake::Task["db:create"].invoke
    abort 'ERROR: Database has no schema version and is not empty' unless ActiveRecord::Base.connection.tables.blank?
    Rake::Task["db:schema:load"].invoke
    retry
  end
end
 

Addendum

Thanks to the Fail Early chapter of the Advanced Rails Recipes book for turning me onto the idea of (at least) checking for an up-to-date schema with an initializer.

Also, make sure to check out this nifty trick that lets you run Rake tasks from within a Rails app. It's pretty easy - all you need to do is load the Rails Rakefile and use .invoke:

 
load "#{RAILS_ROOT}/Rakefile"
Rake::Task["db:create"].invoke
 

P.S. I spent the last hour re-enabling comments on this site, just so somebody like you can tell me that I'm not allowed to do this and why. So, please, have at it!

Update: I updated the regex thanks to a patch that Ben sent to deal with more kinds of directory names.

Update 2: After living with this for a while, I decided to take it out of El Dorado for the time being. I still think it's an interesting idea, but it's too risky to play with production data like this, and I think it's better to be on the safe side. Plus, the Rails developers I've spoken to about it seem to think that it's introducing unexpected behavior into the app (e.g. auto-migrations are not the norm).

 

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

 

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!