Monday, February 25, 2008

Rails Forum Tutorial for Beginners(Part 2 of 3)

This is part 2 of my Rails 2.0 Forum tutorial for beginners. You can find part 1 here. Last time, we created our Forum, Topic, and Reply objects. We nested topics inside of forums and adjusted the views/controllers to allow for the nesting. This time, we will take care of replies being nested in a topic.

The nesting in /config/routes.rb has already been taken care of in the first tutorial:
  map.resources :topics, :has_many => :replies


The first thing we are going to do is to create partials for /app/views/replies/new.html.erb and /app/views/replies/edit.html.erb. Create the file /app/views/replies/_reply.html.erb

#/app/views/replies/_reply.html.erb

<% form_for([@topic, @reply]) do |f| %>
<p>
<b>Subject</b><br />
<%= f.text_field :subject %>
</p>

<p>
<b>Body</b><br />
<%= f.text_area :body %>
</p>

<p>
<%= f.submit button_name %>
</p>
<% end %>


And now change edit and new to:


#/app/views/replies/edit.html.erb

<h1>Editing reply</h1>

<%= error_messages_for :reply %>

<%= render :partial => @reply, :locals => { :button_name => "Submit" } %>

<%= link_to 'Show', [@topic, @reply] %> |
<%= link_to 'Back', replies_path %>



#/app/views/replies/new.html.erb

<h1>New reply</h1>

<%= error_messages_for :reply %>

<%= render :partial => @reply, :locals => { :button_name => "Reply" } %>

<%= link_to 'Back', replies_path %>


Same stuff we did last time...
Now lets go to /app/controllers/replies_controller.rb
Add this at the top, to make sure a topic gets loaded everytime the replies controller is used:

before_filter :load_topic

def load_topic
@topic = Topic.find(params[:topic_id])
end

inside the new function change the line:

@reply = Reply.new

to this:

@reply = @topic.replies.build

Change the edit function:

#old edit function

def edit
@reply = Reply.find(params[:id])
end


#new edit function

def edit
@reply = @topic.replies.find(params[:id])
end

Change the create function:

#old create

def create
@reply = Reply.new(params[:reply])

respond_to do |format|
if @reply.save
flash[:notice] = 'Reply was successfully created.'
format.html { redirect_to(@reply) }
format.xml { render :xml => @reply, :status => :created, :location => @reply }
else
format.html { render :action => "new" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end


#new create

def create
@reply = @topic.replies.build(params[:reply])

respond_to do |format|
if @reply.save
flash[:notice] = 'Reply was successfully created.'
format.html { redirect_to([Forum.find(@topic.forum_id), @topic]) }
format.xml { render :xml => @reply, :status => :created, :location => @reply }
else
format.html { render :action => "new" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end



Notice the change in redirect_to...after a reply to a topic is submitted, I want to send the user back to the forum the topic was in, to get the forum, I had to use the find function and pass it the forum_id of the topic and the topic.


In the update and destroy functions:

#old update and destroy

def update
@reply = Reply.find(params[:id])

respond_to do |format|
if @reply.update_attributes(params[:reply])
flash[:notice] = 'Reply was successfully updated.'
format.html { redirect_to(@reply) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end

def destroy
@reply = Reply.find(params[:id])
@reply.destroy

respond_to do |format|
format.html { redirect_to(replies_url) }
format.xml { head :ok }
end
end



#new update and destroy

def update
@reply = @topic.replies.find(params[:id])

respond_to do |format|
if @reply.update_attributes(params[:reply])
flash[:notice] = 'Reply was successfully updated.'
format.html { redirect_to(@topic) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end


def destroy
@reply = @topic.replies.find(params[:id])
@reply.destroy

respond_to do |format|
format.html { redirect_to(@topic) }
format.xml { head :ok }
end
end


Now that we have all of that done...lets head over to /app/views/topics/show.html.erb so we can display our replies and the form for a new reply.


In addition to showing replies and a form for a new one, I changed the formatting a little, so here are the contents on show.html.erb

<p>
<h3>Forum: - <%=h @forum.name %></h3>
</p>

<p>
<b><%=h @topic.subject %></b>
</p>

<p>
<%=h @topic.body %>
</p>

<% unless @topic.replies.empty? %>

<% @topic.replies.each do |reply| %>
<p>
<b><%= reply.subject %></b>
</p>
<p>
<%=h reply.body %>
</p>

<% end %>
<% end %>

<h2>Reply</h2>
<%= render :partial => @reply = Reply.new, :locals => { :button_name => 'Reply' } %>

<%= link_to 'Edit', edit_forum_topic_path(@forum, @topic) %> |
<%= link_to 'Back', topics_path %>



Since this is the same stuff we were doing in the first tutorial, I won't bother explaining any of it...you should be able to tell what is going on. Feel free to leave a comment if you do have any questions.


Now, fire up the web server and head to localhost:3000/forums/1/topics/1 (assuming you have a topic already created).






And create a couple of replies:





And thats it! (10 points go to anyone who catches the video game reference in reply #2 in the screenshot).

In this tutorial, we nested replies inside of a topic, and set the views/controllers accordingly. Next time, we will take a look at authentication using the RESTful authentication plug-in and acts_as_state_machine plugin.

Feel free to leave comments, and link me from your site. See you next time!

Friday, February 22, 2008

Rails Forum Tutorial for Beginners (Part 1 of 3)

This is my first tutorial, so bear with me. I am writing it as if trying to teach someone brand new to rails what is going on. We are going to be creating a forum site in Ruby on Rails(2.0). I plan on this to be a several-part series, so keep coming back and checking for updates.

So here we go!



rails myforum -d mysql

This will create the project folder 'myforum' and force the database used to mysql(the newest version of rails default to sqlite3). All kinds of files and folders are created, but don't let them overwhelm you. Rails is an application framework, so by creating all this stuff it will force you to keep everything organized according to Rails' conventions.

Now, the general layout I decided on for the forums is this: The site will have multiple forums, each forums is going to have multiple topics, and each topic will have multiple replies. To get this going, lets try this:


cd myforum
ruby script/generate scaffold Forum name:string description:text


More files and directories are created, setting up migration files for the database, a model, a controller, and views for the Forum class. Lets analyze the command we just gave:
ruby script/generate - on a mac or on linux, it would be script/generate(I'm on windows). You will be using script/generate for a lot of things in rails, so get used to typing that one.
scaffold Forum - This tells rails that you want to generate a scaffold for the class 'Forum
name:string description:text - in the forums table that will be created, it will have 2 columns: name, and description. the :string and :text are the type of data, with string being a small 1 liner used for naming, and text being something that can be much longer. Moving on....


rake db:create
rake db:migrate


rake db:create will create your mySQL database for you (called myforum_development). rake db:migrate will create the table for the forum class.

Now lets fire up the server and see what we've got!

ruby script/server

And point your web browser(and you better not be using IE!) to http://localhost:3000


So the server is working fine, now go to http://localhost:3000/forums



If you click on New forum, it will give you a form with fields for Name and Description.





And then click back:




Thats right! With only 4 lines of code, rails was able to generate all of this! The scaffolding will create everything you need to Create, Read, Update, and Delete(CRUD).

Moving on, lets set things up so that rails will show the list of forums page by just going to http://localhost:3000, and get rid of that rails info page. Delete the file public/index.html, and then in config/routes.rb do this:

#In config/routes.rb

ActionController::Routing::Routes.draw do |map|
map.resources :forums
map.root :controller => 'forums', :action => 'index' # <-------add this line

..........
..........
end


So this is our first editing of a rails file. routes.rb is very important to the whole rails applications. They take a request for a URL and turn it into a request from a controller and an action. In this case, the line we added will take a request for the root(http://localhost:3000) and give us back the index action of the forums controller(which will list all the forums). Save, and again, point the browser to http://localhost:3000:




Great! Now, lets go ahead and generate scaffolding for our other 2 classes: Topics and Replies:


ruby script/generate scaffold Topic forum:references user:references subject:string body:text

ruby script/generate scaffold Reply topic:references user:references subject:string body:text

rake db:migrate

We have something new here. forum:references means that a topic will contain a foreign key to the forum it belongs to. It will also contain a foreign key for the user the posts it. Additionally, a reply will belong to a certain topic. And again, rake db:migrate will create the additional tables in the database. Don't worry about the User class yet, that part will be in a later tutorial when we add authentication.

If you go to http://localhost:3000/topics and http://localhost:3000/topics, you will see similar pages to the forum page that was created earlier.

Now that we have our scaffolding up, its time to take a look at some of the code that was generated and put it where we want it. First, lets go to /config/routes.rb and do a few things:


BEFORE:
#In config/routes.rb

ActionController::Routing::Routes.draw do |map|

map.resources :replies
map.resources :topics
map.resources :forums

map.root :controller => 'forums', :action => 'index'
...............
...............
end



AFTER:
#In config/routes.rb

ActionController::Routing::Routes.draw do |map|

map.resources :forums, :has_many => :topics
map.resources :topics, :has_many => :replies
map.resources :replies

map.root :controller => 'forums', :action => 'index'
...............
................
end


This sets up routing so that to view a specific post, you would go to a URL like: /forums/1/posts/3, and to see a reply /posts/1/replies/3.

Moving on, lets edit the files in /app/models/


#forum.rb

class Forum < ActiveRecord::Base
has_many :topics
has_many :replies, :through => :topics
end
------------------------------
#topic.rb

class Topic < ActiveRecord::Base
belongs_to :forum
belongs_to :user
has_many :replies
end
-------------------------------
#reply.rb

class Reply < ActiveRecord::Base
belongs_to :topic
belongs_to :user
end



All of that should make sense to you, just fleshing out some of the stuff we've already talked about.

Next, lets check out /app/controllers/topics_controller.rb. Add these lines to it:


before_filter :load_forum

def load_forum
@forum = Forum.find(params[:forum_id])
end


before_filter :load_forum will cause the load_forum method to be called anytime the topics controller is accessed.


Now, in the index function, change:
@topics = Topic.find(:all)

to
@topics = @forum.topics


Now change every instance of:
@topic = Topic.find(params[:id])

to
@topic = @forum.topics.find(params[:id])

and in the new function:
@topic = Topic.new

to
@topic = @forum.topics.build


Change the create function from:

def create
@topic = Topic.new(params[:topic])

respond_to do |format|
if @topic.save
flash[:notice] = 'Topic was successfully created.'
format.html { redirect_to(@topic) }
format.xml { render :xml => @topic, :status => :created, :location => @topic }
else
format.html { render :action => "new" }
format.xml { render :xml => @topic.errors, :status => :unprocessable_entity }
end
end
end

to

def create
@topic = @forum.topics.build(params[:topic])

respond_to do |format|
if @topic.save
flash[:notice] = 'Topic was successfully created.'
format.html { redirect_to(@forum) }
format.xml { render :xml => @topic, :status => :created, :location => @topic }
else
format.html { render :action => "new" }
format.xml { render :xml => @topic.errors, :status => :unprocessable_entity }
end
end
end


In update, change the line:

format.html { redirect_to(@topic) }

to

format.html { redirect_to(@forum) }

and finally, in destroy, change the line:

format.html { redirect_to(topics_url) }

to

format.html { redirect_to(forum_topics_url(@forum)) }


Alright, lets take a look at all that we just did. With the code that was generated by the scaffold, Rails did not take into account the fact that we need to retrieve our topic classes by a foreign key(forum_id). The majority of the changes were in setting that up correctly. The other changes were in the redirect_to function calls. Whenever I create, update, or destroy a topic, I want it to redirect back to the forum I was on instead of going back to the topic page automatically assigned.

Now, when we go to look at a specific forum, we want it to list out not just the forum title and description, but all the topics associated with that forum. Open up /app/views/forums/show.html.erb


<p>
<b>Name:</b>
<%=h @forum.name %>;
</p>

<p>
<b>Description:</b>
<%=h @forum.description %>
</p>


<%= link_to 'Edit', edit_forum_path(@forum) %> |
<%= link_to 'Back', forums_path %>


Since this is our first look at a view, let go over it a bit.

The <% tags work kind of like <?php tags. They switch out of normal HTML markup to ruby code. Inside these tags are where the magic happens. So a tag with just <% will not display anything, they are useful for doing things like loops. A tag with <%= will output the result into the HTML markup. So the line <%=h @forum.name %> will display the name of the forum. The lowercase 'h' is there escape HTML characters in output.
Change the file to this:


<p>
<b>Forum:</b>
<%=h @forum.name %> - <%=h @forum.description %>
</p>

<% unless @forum.topics.empty? %>
<h2>Topics</h2>
<% @forum.topics.each do |topic| %>
<b><%= link_to topic.subject, [@forum, topic] %></b><br />
<% end %>
<% end %>

<%= link_to 'Edit', edit_forum_path(@forum) %> |
<%= link_to 'Back', forums_path %>



Now one more thing; I want to have a 'Create New Topic' form at the bottom of the page. First, lets take a look at /app/views/topics/new.html.erb and /app/views/topics/edit.html.erb. These 2 files are nearly identical. This is a good time to use the DRY(Don't Repeat Yourself) that Rails is big on. Lets create a new file called _topic.html.erb in the /app/views/topics/ folder and put the following code into it:

<% form_for([@forum, @topic]) do |f| %>
<p>
<b>Subject</b><br />
<%= f.text_field :subject %>
</p>

<p>
<b>Body</b><br />
<%= f.text_area :body %>
</p>

<p>
<%= f.submit button_name %>
</p>
<% end %>

And then change edit.html.erb and new.html.erb to look like this:

#edit.html.erb

<h1>Editing topic</h1>

<%= error_messages_for :topic %>

<%= render :partial => @topic, :locals => { :button_name => "Submit" } %>

<%= link_to 'Show', [@forum, @topic] %> |
<%= link_to 'Back', topics_path %>

----------------------------
#new.html.erb

<h1>New topic</h1>

<%= error_messages_for :topic %>

<%= render :partial => @topic, :locals => { :button_name => "Create" } %>

<%= link_to 'Back', topics_path %>


That cleaned up a good bit of code. Since the only real difference in the 2 forms for those pages was the button name, we created what is called a partial in the file _topic.html.erb, and then rendered the partial in new.html.erb and edit.html.erb, passing in the value of the button name. And now, we can also re-use the partial in /app/views/forums/show.html.erb

Change the file to look like this:


<p>
<b>Forum:</b>
<%=h @forum.name %> - <%=h @forum.description %>
</p>

<% unless @forum.topics.empty? %>
<h2>Topics</h2>
<% @forum.topics.each do |topic| %>
<b><%= link_to topic.subject, [@forum, topic] %></b><br />
<% end %>
<% end %>

<h2>New Post</h2>
<%= render :partial => @topic = Topic.new, :locals => { :button_name => 'Create' } %>

<%= link_to 'Edit', edit_forum_path(@forum) %> |
<%= link_to 'Back', forums_path %>


One more thing, open up /app/views/topics/show.html.erb and remove this part:

<p>
<b>User:</b>
<%=h @topic.user %>
</p>

Since we haven't implemented the authentication yet.

And change the line:
<%=h @topic.forum %>

to
<%=h @forum.name %>

And then change:
<%= link_to 'Back', topics_path %>

to
<%= link_to 'Back', forum_path(@forum) %>

Save everything, fire up the server, and head back to http://localhost:3000
ruby script/server



Click Show



And there we go! You can add a new post, and then click on it to show it.

So far in the tutorial, we have generated scaffolding for 3 classes(forum, topic, reply), set up the nested routing for topics to be inside forums, and replies to be inside topics, and modified the controllers and view for forum and topic to work like we need it to.

I've decided this will be a 3-part tutorial. The next part will be to get the Replies working(it will be very similar to what we did with nesting topics inside forums) by modifying the views for topics, and the views and controllers for replies. The third part will be to set up Authentication and Admin priveleges.

Let me know what you think, or if you have any questions. And tell your friends.

See you next time!

Wednesday, February 20, 2008

Ruby on Rails 2.0 forum

I decided to start work on a forum today. Using the concepts I learned in Akita On Rails's tutorial to route a comment into a blog post can be similarly used to route a post to a forum and a reply to a post. I might look into something more complicated later such as a reply to a reply(digg style). I'm going to start by getting it all working regularly and then attempt to add in some AJAX to make it look spiffy.

Anyway, first I decided that I needed some kind of Authentication for accounts, sessions, and whatnot so I went back to my favorite resource RubyPlus.org and checked out the screencast RESTful Authentication tutorial.

There were a couple small problems I ran into, I'm assuming because there is a new version of act_as_state_machine plugin that has came out since the tutorial that was wrote. I will add that to the post tomorrow, along with a tutorialized version of all the steps I have taken so far.

Oh, and here are some beginner resources/tutorials I have found around the net the past few days:



Well thats it, will update you on my forum app tomorrow. Thanks for coming, and feel free to spread the word!

Monday, February 18, 2008

AWDR depot app for Rails 2.0 (RubyPlus.org) - Part 2

Here goes part 2.

I've finished the screencasts for chapter 8, 9, and 10.

Here are the problems I came across while doing so:

At the beginning of chapter 8, the top of the screen is cut off when creating sessions in the database, the is the whole line:

rake db:sessions:create


And then towards the beginning of chapter 10, a line is cut off in 005_create_line_items.rb

t.decimal :total_price, :null => false, :precision => 8, :scale => 2


I had some trouble with the ajax highlight effect in add_to_cart.js.rjs

page[:current_item].visual_effect :highlight,
:startcolor => "#88ff88",
:endcolor => "#114411"


and was not able to figure it out. When I clicked Add to Cart, I would get the error "TypeError: $("current_item") has no properties" and then another popup window with a lot of code in it. I looked around the internet for a while trying to get it fixed, but nothing I tried worked. If anyone figures this out, let me know. I ended up commenting out that line in add_to_cart.js.rjs.

Everything else works great. I really like Bala's screencasts, they are a great resource for learning Rails 2.0. After finishing them, I believe I know enough to make something that is not too complicated. For my next post, I believe I am going to start working on a forum in Rails, and AJAX it up(I could use a lot more experience with AJAX).

See you all next time!

Thursday, February 14, 2008

New Resource for Beginner 2.0 Tutorials

Just found a site with some great video tutorials for beginners. Its RubyPlus.org. You have to create a free account to access the download.

When I first went to learn Ruby, the book unanimously suggested to me was Agile Web Development with Rails. After a quick search I had found out that the tutorials, of course, didn't work with 2.0. A little more searching and I came across RubyPlus. He has the depot app from the book adapted to Rails 2.0, and video tutorials on it. You will need some of the source that came with the book, which can be downloaded here.

After you create your account at RubyPlus, click Archives to see the tutorials. The AWDR depot ones are 19, 22, 27, 28, 29. It looks like there is a lot of good material on this site besides depot tutorials, I plan to spend a lot of time watching Bala's screencasts.

Also, you can find his blog here.

On to the tutorial:

I've finished the first 2 screencasts without too much trouble. He moves pretty fast, and copies/pastes a lot of code when he creates new files, so be ready to pause so you can type everything out.

In the first screencast, we set up the project, created scaffolding, and played around with the view a little. In the second, we created an admin namespace, and then nested a product route under it, changing the controllers and views so that only an admin could add, edit, and destroy books. So far, much like the blog app from the AkitaOnRails site.

There was a point where you needed some images and css files from the AWDR book (link above), and despite what he says on his page, you don't need to buy the book to download it(although I had the book already). There is also a segment in the first screencast where he is creating a bootstrap.rake file to set up automatic population of the database, and the screen cuts off some of the code he pastes into the file, the full contents of the file are:

SITE_DIR = File.join(RAILS_ROOT, 'themes/site-' + (ENV['SITE_ID'] || '1'))
namespace :db do
namespace :bootstrap do
desc "Load initial database fixtures (in db/bootstrap/*.yml) into the current environment's database.
Load specific fixtures using FIXTURES=x,y"
task :load => :environment do
require 'active_record/fixtures'
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'db', 'bootstrap',
'*.{yml,csv}'))).each do |fixture_file|
Fixtures.create_fixtures('db/bootstrap', File.basename(fixture_file, '.*'))
end
end
end
end


See you next time!

Tuesday, February 12, 2008

Akita on Rails Tutorial

Before I start, I will note that I tried to do a couple of tutorials before I created this blog and because of them being written for pre-2.0 it didn't work out so well, but I got a little bit of an introduction to Rails with them anyway, so I am not completely vanilla when it comes to Rails, just mostly so.

For my first tutorial, I decided to try Rolling with Rails 2.0 - The First Tutorial at AkitaonRails.com.

Now this tutorial assumes that you have used Rails before 2.0, but it seems to break everything down enough for me to know whats going on. I've made it to the section on Namespaced Routes, where I plan to pick back up tomorrow. So far I've not had any trouble besides a couple of typos I had to go back and fix.

A couple of notes:

The newest version of rails defaults to sqlite3 as the database, to force it to use mysql, when you create the project, use:

rails blog -d mysql

instead of:

rails blog

Also, since I'm on Windows(not by my choice, my computer at home runs Ubuntu), on lines where the tutorial runs something from the script folder:
./script/generate
./script/server

I had to use:

ruby script/generate
ruby script/server


Easy enough. I'll be updating this post tomorrow with the rest of part one of this tutorial.

EDIT: Tomorrow

Just finished up part one, adding in admin functionality using Namespaced Routes. Basically we set up all the routing for admin, and copied the views of what was already created, so that for admin rights, you would go to /admin/posts as opposed to /posts. Then we went to the views for the regular posts and stripped out the ability to add/delete/edit posts, and to delete/edit comments.

Alright now for my opinion of the tutorial:

While this was aimed for people with a little experience at Rails, Akita did a great job of not assuming we knew too much, making it easy to follow and understand what was going on.

The amount of effort that it takes to make a small web app such as this is amazing. This is as complicated as some projects that have taken me weeks to program in PHP (I never did learn CakePHP or any of the like). If I had known, I would have been using Rails a while ago. I would have moved on to the 2nd part of the tutorial, but the link seems to be down right now, so I guess it will wait until the next post.

New Blog!

So this is my first blog. I'm going to be learning Ruby on Rails 2.0 so I thought I might start up a blog to document the whole process, maybe help out someone else in the process.

So a quick google tells me that there are several others having my same problem:
PAUSE: while typing this I notice that if you don't capitalize 'google' blogger tells me that it is misspelled - it seems that google(who owns blogger.com) wouldn't mind the name of their company falling under the category of common noun, but back to the problem:

Rails 2.0 was just released, and while there are some excellent resources for beginners to Rails(Agile Web Development with Rails), they are mostly pre-2.0, and due to some of the major changes, will not work. So the choices at this point to newcomers to Rails:

1) Downgrade to an older version and use one of the tutorials.
2) Stick with 2.0 and work through the old tutorials, using internet resources to fix things broken by 2.0
3) Scour the internet for the few 2.0 tutorials for beginners.

I'm going to go with option 3. I'll be posting resources, screenshots, and anything else that I find useful in my journey. Feel free to leave comments if you have any questions, suggestions, or corrections.

Oh, and welcome to Rails on Edge!