Posts Tagged ‘forum’

Rails Forum

Tuesday, February 19th, 2008

Please note: This is now hosted at Github. Please use this page to get the latest changeset.

As a result of Tom (0.01%) and mine’s (99.9%) work on-and-off for a few months, we’ve managed to get to a first release of Rails Forum. The goal is to “be inspired by” and supercede PunBB and I think we’re almost there.

The PunBB about page states that:

Some features that have so far not been implemented are: private messaging, file attachments in posts, polls, linking to off-site avatars, advanced text formatting controls, subforums etc etc. Some of these features might still get implemented, just not in the near future.
We have private messaging, and we use Gravatar for our avatar system and we have subforums. There’s only three and a half things on that list (polls, real off-site avatars, adavanced text formatting controls and file attachments) that we have still to do. Polls are easy enough and probably my next goal, off-site avatars will be a bit tricky but I think we’ll manage and advanced text formatting controls are as easy as installing something like TinyMCE. File attachments can be done with attachment_fu.

What I would like to see is the whole site converted over to HAML as I enjoy working in it’s strictly tabbed environment and it’s just so much easier to read as you’re not reading what is basically the same thing twice. Another thing I would like to see is some bloody tests written for it! We were bad and only wrote a few tests. Pagination would be lovely too!

To run this forum system, you’ll need to download and extract it onto your computer. Then install Ruby and then Rubygems. After that, do

gem install mongrel rails chronic RedCloth tzinfo
To install mongrel and rails. Hopefully that’s all you’ll need.

Restful Routing: An Overview

Sunday, January 6th, 2008

Here’s something I posted on rubyonrails talk:

In truth, restful routing is plain and simple. It’s like those books you wrote when you were a kid in kindergarten that if a book critic were to read them he would jab his eyes out with a pen.

For this example I’m going to use a personal favourite: a forum system. It’s small enough to not be overwhelming, yet large enough to explain how restful routing should work (and generally why you should use it).

It all starts with the good ‘ol config/routes.rb file. In here is where all the nice little routes live, from map.root all the way down to the map.connect ‘:controller/service.wsdl’, :action => ‘wsdl’ that many people still leave in their routes file, thinking that if they remove it the entire world would collapse upon itself into one small quantum singularity. In here you’d place something similar to:

map.resources :forums, :hasmany => :topics
map.resources :topics, :hasmany => :posts
map.resources :posts
Not running on Rails 2.0? Then this is the code you want:
map.resources :forums do |forum|
  forum.resources :topics, :nameprefix => "forum"
end

map.resources :topics do |topic| topic.resources :posts, :nameprefix => "topic" end

map.resources :posts

This defines routes like:
/forums/1
- Show a forum
/forums/1/topics
- Index action for a single forum
/forums/1/topics/1
- showing a single topic
/topics/1
- Same thing
/topics/1/posts/
- I would imagine this would do a similar thing to /topics/1
/topics/1/posts/1/edit
- Allows you to edit a single post
/posts/1/edit
- Same thing

Now to define something like this without the magic of restful routing, one would have to be clinically insane:

map.connect "/forums/:id", :controller => "forums", :action => "show"
map.connect "/forums/:forumid/topics/", :controller => "topics", :action => "index"
map.connect "/forums/:forumid/topics/:id", :controller => "topics", :action => "show"
map.connect "/topics/:id", :controller => "topics", :action => "show" <- Does this look familar to /forums/:id?
map.connect "/topics/:topic_id/posts", :controller => "posts", :action => "index"
map.connect "/topics/:topic_id/posts/:id/edit", :controller => "posts", :action => "edit"
map.connect "/posts/:id/edit", :controller => "posts", :action => "edit"
And this is only the tip of the iceberg!

Seeing a pattern here? Restful routing gives you a whole heap of cool stuff, namely the 7 core methods that I’ll cover right after the models.

A forum system has the following tables: forums, topics, posts and users, and the models would look something like the following:

class Forum < ActiveRecord::Base
  hasmany :topics
  hasmany :posts, :through => :topics
end

class Topic < ActiveRecord::Base
  hasmany :posts
  belongsto :forum
  belongs_to :user
end


class Post < ActiveRecord::Base
  belongsto :topic
  belongsto :user
end


class User < ActiveRecord::Base
  hasmany :topics
  hasmany :posts
  def to_s
    login
  end
end
The fields don’t matter, but throughout the tutorial I make reference to @forum.name or something similar, so we’ll assume forums has at least a name field. We’ll assume post has a text field and users has a login field.

That’ll give you some idea of how the system works: Forum -> Topics -> Posts.

In restful routing there are seven “core” methods (actions) that you’re given for the controllers: index, show, new, create, edit, update, destroy. Each of these have a set request method on them, for example you can’t GET to the create, update and destroy actions and you can’t post to the index, new or edit actions. These actions work with these request methods:

GET: index, show, new, edit POST: create PUT: update DELETE: destroy

“What the?! PUT & DELETE, where did they come from?”, I hear you cry! These are hacked into the calls for the appropriate action using javascript, it passes in one more parameter (_method) which is then handled by the rails code and depending on what method you called you will get the page you were looking for, or a routing error.

The forums controller could look like this:

class ForumsController < ApplicationController
  def index
    @forums = Forum.find(:all)
  end

def show @forum = Forum.find(params[:id]) end

def new @forum = Forum.new end

def create @forum = Forum.new(params[:forum]) if @forum.save flash[:notice] = "You have created a forum!" redirectto forumspath else render :action => "new" end end

def edit @forum = Forum.find(params[:id]) end

def update @forum = Forum.find(params[:id]) if @forum.updateattributes(params[:forum]) flash[:notice] = "You have updated #{@forum.name}" redirectto forum_path else flash[:error] = "This forum could not be updated." render :action => "new" end end

def destroy @forum = Forum.find(params[:id]) @forum.destroy flash[:notice] = "You have deleted #{@forum.name}" redirectto forumspath end

end

You’ll see here that I’ve twice made a call to redirectto using the argument of forumspath. Because we’ve defined map.resources :forums in our config/routes.rb file, it knows that we want to go to { :controller => “forums”, :action => “index” } and the best part is that we don’t have to keep trying { :controller => “forums”, :action => “index” } every time we want to go to that specific action, but instead we type forums_path.

I’ve also made a single call to forum_path, and I haven’t specified an argument for it, so how does Rails know that I want to go to the forum that I just updated?

Rails will see that there’s an argument mission from the forum_path and will go looking for the @forum instance variable you’ve defined in your controller. If you never defined one or defined it as something other than @forum, it will mention something about ambiguous routes and you’ll have to specify the variable.

Now what if you wanted to go to the new or edit action? Simple: newforumpath and editforumpath(@forum) will take you to the corresponding actions. Remember that you don’t need to specify an argument for the editforumpath if @forum is defined. Inside these actions you’ll want to go further, you’ll want to create a new forum and update a forum.

For the create action you could specify this for your form: Rails 2.0:

<% form_for @forum do |f|%>
Rails 2.0 will see that @forum is a new record and link you the create action.

Pre Rails 2.0:

<% form_for :forum, @forum, :url => forums_path do |f|%>
Prior to Rails 2.0 that checking wasn’t in, you’ll have to define your own link.

“B-b-b-ut”, you stammer, “you’ve linked to the forums index, right? Isn’t that what forums_path is?”

Well, yes and no. This has everything to do with the four request methods mentioned previously, because the form’s method attribute is “post”, Rails knows that if you’re posting to forums_path, you mean the create action. And now for the update action!

Here the form_for’s a little different, but only for pre-Rails 2.0:

Rails 2.0:

<% form_for @forum do |f| %>
Again the same deal applies: Rails 2.0 knows that @forum is not a new record, so it’ll link you to to the update action because it’s included in a form. This automatically specifies :html => { :method => :put } for you.

Pre Rails 2.0

<% form_for :forum, @forum, :url => forum_path(@forum), :html => { :method => "put" } do |f| %>
It knows to link you to the update action because of the method => “put” we’ve specified.

Now lets escape from the confines of a single controller and bring the topics controller into the mix. In the forum show action is where you would generally show all the topics for that forum, but for the purposes of this tutorial I will do it in the topics controller instead. This will have something similar defined to the forums controller but personally I would define this for the controller:

class TopicsController < ApplicationController
  beforefilter :getforum

def index @topics = @forum.topics end

#other actions private def getforum @forum = Forum.find(params[:forumid]) if params[:forum_id] end end

The private call makes any method after it private, that means that if you were to try and access this method (without restful routing), it would play dumb. Personally, I think something like this should be in-built to Rails, if you’re accessing a child object (topics) from a parent (forum) it should automatically define @forum for you.

Because we’ve already defined the :hasmany topics on map.resources :forums, the topic routes are already defined for us, so to view all the topics for a forum, before you would have to do define a route like this:

map.connect "/forums/:forumid/topics/", :controller => "topics", :action => "index"
and then call it like this:
{ :controller => "topics", :action => "index", :forumid => @forum.id }
Instead you’ve already defined :hasmany => :topics, so instantly you’ll gain access to forumtopicspath. Again the wonderful Rails will realise that you want all topics for the @forum object and then direct you to “/forums/1/topics/” through forumtopicpath. To edit a single topic, you could do editforumtopicpath as of Rails 2.0, or forumedittopicpath prior to Rails 2.0. The first reads more like “edit this topic belonging to this forum” where the second reads like “in this forum, edit this topic”. Alternatively you could ditch the whole forum part out of the method call and just do edittopicpath because we’ve defined map.resources :topics.

Throwing one more controller into the mix now, called postscontroller. This would be very similar to the topics controller but instead of getforum it would have get_topic, modified correctly.

Now what if you wanted to add a custom action to posts_controller, called quote? This action would bring up a form with the post you were quoting which would then send the information from the form into posts/new to create a new post:

config/routes.rb

map.resources :posts, :member => { :quote => :get }
The extra argument of :member indicates a hash of any further actions and their request methods you would like to be added onto singular posts. You can call these like all the other singular methods: quotepostpath(@post), for example. The request methods can be in string or symbol format, it doesn’t matter.
def quote
  @oldpost = Post.find(params[:id])
  @post = Post.new
  @post.text = "[quote='#{@oldpost.user}']#{@old_post.text}[/quote]"
  render :action => "new"
end
Here I’ve defined the old post only to get the text and the user’s name from it, and then we’re rendering the new view so we have the form. Everything from there on is taken care by Rails.

Now what if you want to define a new action to work with a group of posts? Well you define it like this: routes.rb

map.resources :posts, :member => { :quote => :get }, :collection => { :destroyall => :delete }
Now if you wanted to destroy all posts for a topic, bar the first one, with an action like this (remembering @topic is defined in gettopic): postscontroller.rb
def destroyall
  @posts = @topic.posts  - @topic.posts.first
  @posts.each { |post| post.destroy }
  flash[:notice] = "All posts have been deleted.
end
You would linkto it like this:
<%= link_to "Delete all posts", destroy_all_topic_posts_path(@topic), :method => "delete", 
:confirm => "Are you sure you want to delete all posts from this topic?" %>
The :method => “delete” corresponds with the :delete
all => :delete we specified in config/routes.rb.

One last thing is nested form_fors. Say you want to edit a post within a topic. To do this you would use this code:

<% form_for [@topic, @post] do |f| %>
  <%= render :partial => "form", :locals => { :f => f } %>
  <%= submit_tag "Update" %>
<% end %>

Here you pass one argument still to form_for, an array. It passes the array and will give you a url like /topics/1/posts/1 if you were editing an existing post, or /topics/1/posts if you were creating a new post.