NoobOnRails

Ruby on Rails tips, tricks and hints for the aspiring rails hero.



Wednesday, February 21, 2007

Freezing to latest edge rails will break your app


But only for a brief second! The culprit? The switching of the default Rails session management method over to a cookie based management system. Right after I froze to edge, I started my app up and immediately tried to access it. Blam, it blew up in my face. Right there in the server console it was telling me I needed to stick

session :secret => "secret phrase here"


in my Application.rb. Doing that made everything better. You can read and dig into all what Jeremy added in this change set here.

acts_as_list makes lists drop dead easy


Using ActiveRecord's acts_as_list makes dealing with listed items a breeze. A lot of the examples I have seen assume that you'd be using acts_as_list on a parent's child items, the belongs_to side of a has_many relationship. Chances are, most of the time you'll be using acts_as_list, you'll be using it in the same situation but you can use it on a childless object as well. Here's the code...

For this example, I'll go with a list of Books that I am displaying in a list, I have a Book.rb model and a Books controller and I'm listing books in my Books < style="font-weight: bold;">script/generate add_position_to_books_table

That creates the actual migration file. The insides of that file could look something like this...


class AddPositionToBook <>
def self.up
add_column :books, :position, :integer
#i didn't add a default becuase I'm going to set one in the controller

#if I currently had a list of books, I want to go through them and line them up
#so I need to reset the column information for the books table so that I can
#acutally use the position column in this same migration file
Book.reset_column_information


#have to grab all the books so I can cycle through them
book = Books.find(:all)

#iterate through the books and for each one, grab the actual book and it's
#position in the array so I can use their position to set their position, if
#that makes sense
book.each_with_index do |b, i|
b.position = i+1
#save it with the bang so it I did something naughty, it'll blow up on me
#it's not necessary b.save should work too
b.save!
end

end

def self.down
#remove the column I just added because not every migration is perfect
remove_column :books, :position
end
end


Cool. Now we can rake db:migrate from the command line to get the column in our database. Now we have the column, let's put it to work.

In my Book.rb model, I can stick acts_as_list near the top for readability. Something like...

class Book <>
acts_as_list
#other stuffs like validations, spiffy methods etc
end


will work. Hot, we're so close...kind of. Now Let's look at the actual list view.

In my view I'm keeping things real simple. I just have all my books listed as s in one big table, nothing special. of course I have the table and th declarations above this but here...


<% for book in @books %>
<tr>
<td><%=h book.title %></td>
<td><%=h book.description</td>
</tr>
<% end %>



...is where the real magic happens. Now let's throw our position goodies in there.

In my view, maybe after my description <td>, I can add a section for position. I would also have to add the corresponding <th> header but I'll just show you the position row...

<td><%=h book.position %> </td>

Ok so the positions are now showing on the list. But if I think about it, I also need to add it to the form for when I add a new book. It would also be cool if I could also have it fill with what should be the last position. So let's add a field to the form for position and then move on to the controller. Here's the field in the form partial...

<td></td>
<td><%= text_field 'book', 'position' %></td>


Cool. Now I have it in the form and I could submit a test one so I could see it create successfully in the server log. But I want it to already be filled in when I first get to the new book form. I want it to prefill with the last position number. I'm picky like that. Here's how I made it happen...

in the new method...


def new

#grab all of the books so we know how many we have
b = Book.find(:all)

#create a new book object because this is the new method
@book = Book.new

#if we have less than one book, let's just set this position value to 1
if b.size < 1
@book.position = 1
else

#if we have more than one book, let's get the position of the last book
#and set this new book's position (in the form field at least), to
#1+ that value
@book.position = b.last.position+1
end
end




Now we move into the Create method to make sure our users don't try to put in crazy spaced out values. If my last book is at a position of 6, I don't want to be able to put in something with the position of 4535. So, in my create method in my Book controller, I put this...


def create
#first with grab the book params hash from the request sent in by the form
@book = Book.new(params[:book])

#get an array of all of the books just so I can find out how many I have
#this is probably would not be the best use of resources if I had
#millions of books, beats me
b = Book.find(:all)

if @book.save
#if the book is saved, let's check it's position against that array
#of books we just created
if @book.position > b.last.position+2

#if the books position is greater than my last book in my list
#change it to fit right after the last one, for example
#if my last item has a position of 6, let's set this new one to
#a position of 7
@book.position = b.last.position+1
end
flash[:notice] = 'Your Book was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end



Cool. Now that's working. But currently, the only way to edit the position is to manually edit each book. We're better than that. Let's add some buttons to in the list view that will let us move the books up or down the list.

In the list view, I turn this...

<td><%=h book.position %> </td>

into this...


<td><%=h book.position %>

<% unless @book.first.position == book.position %>

<%= link_to "up", { :action => 'move', :method => 'move_higher', :book_id => book.id } %>
<%= link_to "top", { :action =>'move', :method => 'move_to_top', :book_id => book.id } %>

<% end %>

<% unless book.last? %>

<% unless book.position == (@books.first.position || @books.last.position) %> | <% end %>

<%= link_to "down, { :action => 'move', :method => 'move_lower', :book_id => book.id } %>
<%= link_to "bottom, { :action => 'move', :method => 'move_to_bottom', :book_id => book.id } %>

<% end %>

</td>




Woah. Now what the heck is that doing right? If you want to know, either go grab Rob Orsini's Rails Cookbook from which I grabbed most of this code. The links, each one really, call the 'move' method in our books controller as well as pass a method param which specifies which acts_as_list method we want that link to trigger. We also pass the book_id so our app knows which book to move. "But there's no move method in the books controller!!" I can hear you scream. Don't fret, here it is...



def move

if ["move_lower", "move_higher", "move_to_top", "move_to_bottom"].include?(params[:method]) and params[:book_id] =~ /^\d+$/
#if the incoming params contain any of these methods and a numeric book_id,
#let's find the book with that id and send it the acts_as_list specific method
#that rode in with the params from whatever link was clicked on
Book.find(params[:book_id]).send(params[:method])
end
#after we're done updating the position (which gets done in the background
#thanks to acts_as_list, let's just go back to the list page,
#refreshing the page basically because I didn't say this was an RJS
#tutorial, maybe next time
redirect_to :action => :list
end



And that is it! Clicking on the up, top, down, bottom buttons on your list page will move the items up and down. If you need to tweak acts_as_taggable just a touch, mess with the scope or anything like that, check out what the rails edge docs have to say here.

Goodbye .rhtml, we knew you well


So I hope you didn't have any emotional attachments to the .rhtml extension because accroding to the recent changeset 6178 it is on it's way out. Say goodbye to the the .rhtml and .rxml extensions and hello the the .erb and .builder extensions. Why, you ask? as to make it a point that the extension shouldn't determine the content. Here's the message form the changelog...

Added .erb and .builder as preferred aliases to the now deprecated .rhtml and .rxml extensions [Chad Fowler]. This is done to separate the renderer from the mime type. .erb templates are often used to render emails, atom, csv, whatever. So labeling them .rhtml doesn't make too much sense. The same goes for .rxml, which can be used to build everything from HTML to Atom to whatever. .rhtml and .rxml will continue to work until Rails 3.0, though. So this is a slow phasing out. All generators and examples will start using the new aliases, though.


So you don't have to completely stop using it right this very second today since you have until Rails 3 dot Oh to phase out the old extensions but it wouldn't hurt to start getting used to the new extensions.

Labels: ,