Showing posts with label rails. Show all posts
Showing posts with label rails. Show all posts

Monday, March 30, 2009

Rails Video Application

EDIT: Demo up and running, the URL is http://videoapp.railsonedge.com/

Earlier this semester, I decided I wanted to keep working on my Video Application(see the Flash Video Tutorial). So when my databases teacher let us decide what our semester project would be, I signed up to do a YouTube-like site.

Since the tutorial, I've added a lot of things: User Authentication(authlogic), Video Thumbnails(using ffmpeg), Pagination(will_paginate), Tags/Tag Cloud(acts_as_taggable_on_steroids), and I wrote up some quick code to take care of user replies. I also added a little bit of CSS to the application so it didn't look so plain. It looks a lot better, but I'm still working on some of it. You can checkout the code for it on GitHub(http://github.com/balgarath/video-app/tree/master). The code from the previous tutorial is in the 'Tutorial' branch.

Anyway, I will have a demo of the application up sometime the next couple days. I'll get the URL posted here. Feel free to fork the repo and play around with the app.

Thursday, January 22, 2009

Flash Video Tutorial with Rails, ffmpeg, FlowPlayer, and attachment_fu

Quick Tutorial today to get a simple Video Model setup and going with Flowplayer today. We want to be able to upload videos and convert them to flash .flv files. For this I'll be using ffmpeg. Other plugins in this tutorial include acts_as_state_machine and Rick Olsen's attachment_fu. Lets get started!



Note: You can find the source code for this app on github at http://github.com/balgarath/video-app/tree/master. EDIT: I've been working on this, so if you want to see the code specific to the tutorial, click on the Tutorial branch on github.

1. Setup



First, lets create an empty app



rails videoapp -d mysql


Do all your basic setup...(edit database.yml, remove public/index, ...). In routes.rb, add this line:


  map.root :controller => "videos"
map.resources :videos

The first line maps '/' to the videos controller, and the next establishes RESTful routes for videos.


Now we need to figure out what flash video player we want to use. For this tutorial, I will be using FlowPlayer, an open-source flash video player. You will need to download flowplayer.zip from http://flowplayer.org/download/index.html. Unzip it to /public. Grab the /example/flowplayer-3.0.2.min.js file and put it in /public/javascripts/flowplayer.js. Put example/style.css in /public/stylesheets.



Next we need to install attachment_fu to handle file downloads.



./script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/


And you will need ffmpeg for converting uploaded file to .swf. This works for me in Ubuntu...


sudo apt-get install ffmpeg


And last, lets install the acts_as_state_machine plugin. We will be using it to keep track of the current state of conversion for the video:


 ./script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/




2. Database/Model Setup




Now we are ready to set up the model. First, run this command to generate the files for you:



  ./script/generate model Video


This last line will also generate a create_video migration in db/migrate/. Open up that file and put this in it:




class CreateVideos < ActiveRecord::Migration
def self.up
create_table :videos do |t|
t.string :content_type
t.integer :size
t.string :filename
t.string :title
t.string :description
t.string :state
t.timestamps
end
end

def self.down
drop_table :videos
end
end



Content_type, size, and filename are used by the attachment_fu plugin. The state field will be used by the act_as_state_machine plugin to keep track of video converting.



Lets move on to the model; open up /app/model/video.rb and add this into it:




has_attachment :content_type => :video,
:storage => :file_system,
:max_size => 300.megabytes

#turn off attachment_fu's auto file renaming
#when you change the value of the filename field
def rename_file
true
end

This is for the attachment_fu plugin. I came across a feature of attachment_fu: when you change the filename of an attachment, the old file automatically gets moved to whatever the new filename is. Since I am creating a new file and changing the filename attribute of the video the reflect that, I don't need this on...so I just overrode the method in the Model.



Since we will be using the acts_as_state_machine plugin to keep track of the state of conversion for videos, lets go ahead and add in the states we will be using. Add this to video.rb:




#acts as state machine plugin
acts_as_state_machine :initial => :pending
state :pending
state :converting
state :converted, :enter => :set_new_filename
state :error

event :convert do
transitions :from => :pending, :to => :converting
end

event :converted do
transitions :from => :converting, :to => :converted
end

event :failure do
transitions :from => :converting, :to => :error
end

Note: I got some of this part from Jim Neath's tutorial.

Now we can use @video.convert!, @video.converted!, @video.failed! to change the state of a particular Video. The last code we need to add to the model is this:





# This method is called from the controller and takes care of the converting
def convert
self.convert!
success = system(convert_command)
if success && $?.exitstatus == 0
self.converted!
else
self.failure!
end
end

protected

def convert_command

#construct new file extension
flv = "." + id.to_s + ".flv"

#build the command to execute ffmpeg
command = <<-end_command
ffmpeg -i #{ RAILS_ROOT + '/public' + public_filename } -ar 22050 -ab 32 -s 480x360 -vcodec flv -r 25 -qscale 8 -f flv -y #{ RAILS_ROOT + '/public' + public_filename + flv }

end_command

logger.debug "Converting video...command: " + command
command
end

# This updates the stored filename with the new flash video file
def set_new_filename
update_attribute(:filename, "#{filename}.#{id}.flv")
update_attribute(:content_type, "application/x-flash-video")
end




A lot of stuff going on here. Convert will be called from the create action. When called, it sets the state of the video to 'convert' and runs ffmpeg to convert the file to flash(.flv). It will then mark the file as either 'converted' or 'failed'.



Now that that is all done, we can create our database and run the migrations:




rake db:create
rake db:migrate


3. Controller/Views



Now we can generate our controller, model, and view files:



 ./script/generate controller videos index show new


Open up /app/controllers/videos_contoller.rb and put in this code:




class VideosController < ApplicationController


def index
@videos = Video.find :all
end

def new
@video = Video.new
end

def create
@video = Video.new(params[:video])
if @video.save
@video.convert
flash[:notice] = 'Video has been uploaded'
redirect_to :action => 'index'
else
render :action => 'new'
end
end

def show
@video = Video.find(params[:id])
end

def delete
@video = Video.find(params[:id])
@video.destroy
redirect_to :action => 'index'
end
end



In the create method, notice that if the video save is true, @video.convert gets called, which convert the video to .flv.



Views



Create the file /app/views/layouts/application.html.erb and put this in it:




<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>
<title>Video Tutorial</title>
<%= stylesheet_link_tag 'style' %>
<%= javascript_include_tag 'flowplayer' %>
</head>

<body>
<h1>Video Tutorial</h1>

<% flash.each do |key,value| %>

<div id="flash" class="flash_<%= key %>" >
<span class="message"><%= value %></span>
</div>

<% end -%>

<%= yield :layout %>

</body>

</html>



Now for our index view (/app/views/videos/index.html.erb)




<h2>Videos</h2><br />

<%= link_to 'New Video', new_video_url %><br />
<% for video in @videos do %>
<p><%= link_to video.title, video %></p>
<% end %>


/app/views/videos/new.html.erb:




<h2>New Video</h2><br />

<% form_for(@video, :html => { :multipart => true }) do |f| %>
<%= f.error_messages %>
<table>
<tr>
<td><%= f.label :title %>: </td><td><%= f.text_field :title %></td>
</tr>
<tr>
<td><%= f.label :description %>: </td><td><%= f.text_area :description %></td>
</tr>
<tr>
<td><%= f.label :video %>: </td><td><%= f.file_field :uploaded_data %></td>
</tr>
<tr><td><%= f.submit 'Submit' %> - <%= link_to 'Back', videos_path %></td></tr>
</table>

<% end %>



This is the upload form. Notice I used f.file_field :uploaded_data...this is for attachment_fu to work. Next is /app/views/videos/show.html.erb:




<h1><%= @video.title %></h1>
<p><%= @video.description %></p>

<a
href="<%= @video.public_filename %>"
style="display:block;width:400px;height:300px"
id="player">
</a>

<!-- this will install flowplayer inside previous A- tag. -->
<script>
flowplayer("player", "/flowplayer/flowplayer-3.0.3.swf");
</script>



And that should do it. ./script/server and try uploading a movie file and see if it works. Also, you could probably mess around some with the call to ffmpeg and increase video quality. There are some good posts if you search for 'ffmpeg' over at the FlowPlayer Forums, and if you purchase a commercial license for the player, you can remove the advertising and get some new features, as well as support. Thanks for reading!



Update:


For a quick way to put the video conversion on a background process, I was able to use Tom Anderson's Spawn plugin. Note that this wouldn't be a very efficient way to scale it if you expect to have a lot of users uploading at the same time, as it forks another process to handle the conversion. This does work well if there aren't a bunch of videos getting uploaded at the same time. If you do need to scale the uploading, I recommend using Ap4r(Asynchronous Processing for Ruby). Here we go!


First, lets install the Spawn plugin from github:


./script/plugin install git://github.com/tra/spawn.git

And next, change the convert method in Video.rb to this:



# This method is called from the controller and takes care of the converting
def convert
self.convert!

#spawn a new thread to handle conversion
spawn do

success = system(convert_command)
logger.debug 'Converting File: ' + success.to_s
if success && $?.exitstatus == 0
self.converted!
else
self.failure!
end
end #spawn
end

And thats it! Now when you upload a video, you don't have to wait for the server to convert the video file over to flash.

Friday, August 15, 2008

Juggernaut Push Server


Juggernaut is a Rails plugin that allows you to push information out to the client, without them having to specifically request it. For certain domains, a push server is an excellent solution. Juggernaut uses a small bit of Flash to open up a flashxmlsocket from the browser to the push server, and it subscribes to the service. My roommate Taelor has been playing around with it for a little while, and has put together a chat application that creates rooms off of Digg topics. Its called Shovel Chat. He has also made a few blog posts about Juggernaut and using it to create a chatroom with rails at his blog, and has posted the source code on GitHub(note that you will need to go through his blog posts and edit some of the gem's files...)


So, the way the chat app works is that when you request the show action for a chatroom, some flash gets sent to subscribe to the channel on Juggernaut. Juggernaut will then send you a list of all the people in the room. If you type in a message and hit enter, the message gets posted to your Rails app. Inside the controller, the message gets sent to the Juggernaut server, which then pushes the message out to anyone subscribe to that channel. You could achieve a similar effect using AJAX, but you would have to have each client polling the server for updates to the chatroom, but this would be really difficult to scale.


Anyway, I've gotten his source code installed on my machine and have begun playing around with it, and hope to start contributing to his chat_sandbox on github soon. Feel free to make your own fork of his code on github and start contributing, and stay tuned for more updates!

-Ralph

Friday, May 2, 2008

Using Symbols

If you all are anything like me, symbols confuse the hell out of you. You may use them, but you don't really understand them. I decided to research it out a little today and see what I could figure out. It turns out that there are a lot of different viewpoints of symbols out there. Here are some articles I came across:



The first is a detailed overview of symbols, the second shows some of the way Rails uses symbols.


So, as a quick summary, here is what I know about symbols:
A symbol is used to represent strings, without the overhead of actually creating a string. They don't have the functionality that you get with a string, and the value of them can't be changed, but they are much more efficient than using a string. Here is part of the example from the Gluttonous article:



> patient1 = { "ruby" => "red" }
> patient2 = { "ruby" => "programming" }
> patient1.each_key {|key| puts key.object_id.to_s}
211006
> patient2.each_key {|key| puts key.object_id.to_s}
203536

The key in both hashes creates(string "ruby") has a string object created each time. So, in this example, we have 2 seperate string objects with the same name. You can achieve the same result with symbols:



> patient1 = { :ruby => "red" }
> patient2 = { :ruby => "programming" }
> patient1.each_key {|key| puts key.object_id.to_s}
3918094
> patient2.each_key {|key| puts key.object_id.to_s}
3918094

The difference here is that the same symbol is referenced in both of these. Not only is symbol a smaller object, but it gets reused.


So I am curious, what ways do you all use symbols?



-Ralph

Thursday, May 1, 2008

Intro to the Rails Console

Hi all. Today I'm going to give you a bit of an introduction to the Rails console. Using it will make your life a lot easier. To open it up, type ./script/console from your project directory. From here you can play around, test out commands, add objects to the database if you haven't created an page to do it yet. I'll be using the console from my Forum tutorial to give you examples, so here we go!


First, let's load all of our topics from the database into a variable:



>> @topics = Topic.find(:all)

Which outputs the following:



=> [#<Topic id: 1, forum_id: 1, user_id: nil,
subject: "My Rails 2.0 Forum Tutorial",
body: "Check it out, http://railsonedge.blogspot.com",
created_at: "2008-03-06 11:11:07",
updated_at: "2008-03-06 11:11:07">,
#<Topic id: 2,
.......

Easy stuff. Now lets take that hash, and make it into something useful:



y @topics

The 'y' function outputs everything in yaml format, much more readable:



---
- !ruby/object:Topic
attributes:
updated_at: 2008-03-06 11:11:07
body: Check it out, http://railsonedge.blogspot.com
subject: My Rails 2.0 Forum Tutorial
id: "1"
forum_id: "1"
user_id:
created_at: 2008-03-06 11:11:07
attributes_cache: {}

- !ruby/object:Topic
attributes:
updated_at: 2008-03-06 11:11:37
body: Check it out! http://railsonedge.blogspot.com
subject: New Blog Post Today!!
id: "2"
forum_id: "1"
user_id:
created_at: 2008-03-06 11:11:37
attributes_cache: {}

- !ruby/object:Topic
attributes:
updated_at: 2008-03-09 00:35:44
body: Welcome to the new forum.
subject: Welcome!
id: "3"
forum_id: "2"
user_id:
created_at: 2008-03-09 00:35:44
attributes_cache: {}

- !ruby/object:Topic
attributes:
updated_at: 2008-03-09 00:36:04
body: What time is it?
subject: "Question:"
id: "4"
forum_id: "2"
user_id:
created_at: 2008-03-09 00:36:04
attributes_cache: {}

Now, lets grab just one of the topics and play around with that:



@topic = Topic.find(1)

You can use the 'y' function on this also. Try this to display certain attributes of @topic:



>> @topic.body
=> "Check it out, http://railsonedge.blogspot.com"

And you can change these attributes pretty easily also:



>> @topic.body = "And now the body says this."
=> "And now the body says this."
>> @topic.body
=> "And now the body says this."

When you change an attribute, it doesn't automatically save it to the database. But not to worry, it is easy enough:



>> @topic.save
=> true

Additionally, if you are working on a project and don't have a create page set up yet for an object yet, you can just create one from the console:



>> topic = Topic.new :body => "Lorem Ipsum",
:subject => "This Subject", :forum_id => 1, :user_id => 1
=> #<Topic id: nil, forum_id: 1, user_id: 1,
subject: "This Subject", body: "Lorem Ipsum",
created_at: nil, updated_at: nil>
>> topic.save
=> true

That is enough for some basic manipulation of things in your database, but what other things can you do with the console? What if you wanted to see all of the functions that can be used on a specific object? Well, just like when you are using bash, you can use tab to auto-complete:



String.to_ [press tab not enter]

String.to_a String.to_s
String.to_enum String.to_yaml
String.to_json String.to_yaml_properties
String.to_param String.to_yaml_style
String.to_query

Another good thing to use the rails console for is to test out little bits of code to see how they run. Here is a simple example using a loop in the console:



>>@topics = Topic.find(:all)
>> for topic in @topics do
?> puts topic.subject + " - append this"
>> end

My Rails 2.0 Forum Tutorial - append this
New Blog Post Today!! - append this
Welcome! - append this
Question: - append this

As with most terminals, you can access your command history by using the up arrow.
Another cool thing, when you are messing around with routing, you can check things out quickly with app.url_for and app.get:



>> app.url_for(:controller => 'forums', :action => 'index')
=> "http://www.example.com/forums"

>> app.get '/forums/1/topics'
=> 200

>> app.get '/invalid/url'
=> 404


The underscore('_') character will return the value of the last statement:



>> 5 + 9
=> 14
>> _
=> 14
>> _ + 6
=> 20
>> _
=> 20

Well, that is it for now. Let me know what other uses you guys (and girls?) have for the console.


Later,


-Ralph

Friday, April 25, 2008

DRY Up Your Views With Layouts For Your Partials

I got comments on my previous post, DRY up your views with nested partials, letting me know that there is a better way. So here is the same thing, done the proper way:


First, I've got this cool set of divs that gives me a nice looking window I will be reusing:


 
<div id="window-top"></div>
<div id="window">
<%= render :partial => 'mypartial' %>
</div>
<div id="window-bottom"></div>

<div id="window-top"></div>
<div id="window">
<%= render :partial => 'myotherpartial' %>
</div>
<div id="window-bottom"></div>

The goal here is to be able to re-use the code for the window multiple times. First, lets create a partial called '_lorem.html.erb':



<p>"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est
laborum." </p>

And another called '_blah.html.erb':



<p>blah blah blah blah blah blah blah blah blah blah blah blah blah
blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah
blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah
blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah
blah blah blah </p>


Alright! Now, make one last partial, which will be used for the layout, called '_window.html.erb'. Note that it needs to be created in the same directory as the other partials. This is where you put the code for the cool window you just made.



<div id="window-container">
<div id="window-top-left"></div><div id="window-top">Window Title</div>
<div id="window-top-right"></div>
<div id="window">
<%= yield %>
</div>
<div id="window-bottom-left"></div><div id="window-bottom">
</div><div id="window-bottom-right"></div>
</div>

And now, when you want to render a partial that is inside your window, all you need to do is set the layout to window:



<h1>Test of nested partials</h1>

<%= render :partial => 'blah', :layout => 'window' %>

<%= render :partial => 'lorem', :layout => 'window' %>


And thats it! Enjoy DRYing up your views. Later

-Ralph



EDIT: if you use :layout => 'layouts/window', you can put '_window.html.erb' in /views/application and reuse it throughout the site.

Monday, March 24, 2008

Legacy MySQL Database - TinyInt

Apparently Rails typecasts MySQL tinyint's as booleans. I was working on a Rails app for a legacy database today when I realized that I couldn't pull the value out of my 'terms' column. All it would read is a true or false, and not the actual value. After a bit of research, I discovered before_type_cast accessor. So instead of using report.terms, in order to get the value out, I had to use report.terms_before_type_cast.


Short post, but I'm sure someone will have the same problem at some point and will find it helpful.



Later.

-Ralph

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!

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.