Wednesday, March 05, 2008

Integrate a forum with a Rails site

So you've got a fancy rails site where Users can log in and do stuff. And you'd like them to be able to host a community forum on the site, using your existing User model to authenticate users for posting, moderation, and administration. The Savage Beast 2.0 engine does just that. In typical Rails style, you just follow its conventions for determining the logged-in user and authenticating them, and you're good to go with a mini-Beast forum.

When I tried this out yesterday, I ran into tons of issues with the Engines plugin and the way that Rails development environment loads, unloads, and reloads classes. So here are a couple of things that I figured out:

  • First, I don't know thing one about Engines, other than they sound pretty useful. For some reason, after installing the engines plugin, I couldn't run my migrations. It looks like Engines tries to redefine the db:migrate task into a namespace that then migrates all of the plugins. But that didn't work with my version of Rake, so my workaround is just to migrate before installing Engines.

    *NOTE: I was using Rake 0.7.3, but when I tried using Rake 0.8.1, I didn't have the same error.
  • Lots of issues with modules being unloaded in development. So again the solution is to avoid the problem, and run in production mode.

    *NOTE: This is actually due to Rails Dependencies module and the way that Models and Controllers are loaded and unloaded in between requests, and it supposedly happens with lots of different plugins in development. Basically, if your plugin defines models or controllers, you have to call "unloadable" inside the class. Otherwise Rails doesn't unload the class at the end of the request, but does unload referenced modules in your main app. So when Savage Beast's ForumsController calls methods of AuthenticatedSystem included in ApplicationController, the AuthenticatedSystem module gets unloaded from ApplicationController but not ForumsController, and on the next request it won't be reloaded because it is still defined.
  • I also have the old 1.2.3 Rails on my work machine, since I'm maintaining old apps that I don't have time to migrate to Rails 2.0, so as you'll see, I'm explicitly freezing the 2.0.2 gems to vendor/rails.

Here's my install script. If you follow along (and none of the dependent plugin trunks have changed in a way that breaks things), you should be all set.


# steps to create a sample app for Savage Beast 2.0 using Restful Authentication as the User model
# requires: rails, svn
# note: better to run in Production environment than development, because development will cause
# Models and Controllers to unload in between requests, causing unexplained missing constants and methods
rails savage_beast_test
cd savage_beast_test
rake rails:freeze:edge TAG=rel_2-0-2
cd ..
ruby savage_beast_test/vendor/rails/railties/bin/rails savage_beast_test
# to overwrite all, type: a
cd savage_beast_test
ruby script/plugin install restful_authentication
ruby script/generate authenticated user sessions
cd vendor
cd plugins
svn checkout http://savage-beast-2.googlecode.com/svn/trunk/ savage_beast
cd ..
cd ..
cp vendor/plugins/savage_beast/db/migrate/001_create_savage_tables.rb db/migrate/002_create_savage_tables.rb
rake db:migrate
rake db:migrate RAILS_ENV=production
ruby script/plugin install white_list
ruby script/plugin install white_list_formatted_content
ruby script/plugin install acts_as_list
ruby script/plugin install svn://errtheblog.com/svn/plugins/gibberish
ruby script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
ruby script/plugin install http://svn.rails-engines.org/engines/trunk



# *** app file modifications: ***

# config/environment.rb:
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
# Boot the Engines plugin
require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
#...

# config/routes.rb:
ActionController::Routing::Routes.draw do |map|
map.from_plugin :savage_beast

map.resources :users

map.resource :session
#...

# app/models/user.rb:
include SavageBeast::UserInit
def display_name
login
end
def admin?
true
end
def currently_online
false
end
protected
#...



# app/controllers/application.rb:
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
include AuthenticatedSystem
def admin?
logged_in?
end
#...


# *** minor fix to Savage Beast application_helper.rb:
# (note: restful_authentication#current_user returns :false instead of nil)
# vendor/plugins/savage_beast/app/helpers/application_helper.rb:
def beast_user_name
#(current_user ? current_user.display_name : "Guest" )
(logged_in? ? current_user.display_name : "Guest" )
end


ruby script/server -e production

# get: http://localhost:3000/forums
# - first hit causes an error, lowpro.js missing. Second hit works.
# get: http://localhost:3000/forums/new
# - redirects to /sessions/new
# get: http://localhost:3000/users/new
# create a new user... logs in and redirects to /forums/new with "Thanks for signing up!" flash
# create a new forum... redirects to /forums/1 with no topics
# click New Topic link, gets /forums/1/topics/new
# create new topic/first post, redirects to /forums/1/topics/1