Adding Sort Capabilities to a Model with acts_as_listProblemYou need to present the data in a table sorted according to one of the table's columns. For example, you are creating a book and have a database to keep track of the book's contents. You know the chapters of the book, for the most part, but their order is likely to change. You want to store the chapters as an ordered list that is associated with a book record. Each chapter needs the ability to be repositioned within the book's table of contents. SolutionFirst, set up a database of books and chapters. The following migration inserts an initial book and some recipes associated with it: db/migrate/001_build_db.rb: class BuildDb < ActiveRecord::Migration def self.up create_table :books do |t| t.column :name, :string end mysql_book = Book.create :name => 'MySQL Cookbook' create_table :chapters do |t| t.column :book_id, :integer t.column :name, :string t.column :position, :integer end Chapter.create :book_id => mysql_book.id, :name => 'Using the mysql Client Program', :position => 1 Chapter.create :book_id => mysql_book.id, :name => 'Writing MySQL-Based Programs', :position => 2 Chapter.create :book_id => mysql_book.id, :name => 'Record Selection Techniques', :position => 3 Chapter.create :book_id => mysql_book.id, :name => 'Working with Strings', :position => 4 Chapter.create :book_id => mysql_book.id, :name => 'Working with Dates and Times', :position => 5 Chapter.create :book_id => mysql_book.id, :name => 'Sorting Query Results', :position => 6 Chapter.create :book_id => mysql_book.id, :name => 'Generating Summaries', :position => 7 Chapter.create :book_id => mysql_book.id, :name => 'Modifying Tables with ALTER TABLE', :position => 8 Chapter.create :book_id => mysql_book.id, :name => 'Obtaining and Using Metadata', :position => 9 Chapter.create :book_id => mysql_book.id, :name => 'Importing and Exporting Data', :position => 10 end def self.down drop_table :chapters drop_table :books end end Set up the one-to-many relationship, and add the acts_as_list declaration to the Chapter model definition: app/models/book.rb: class Book < ActiveRecord::Base has_many :chapters, :order => "position" end app/models/chapter.rb: class Chapter < ActiveRecord::Base belongs_to :book acts_as_list :scope => :book end Next, display the list of chapters, using link_to to add links that allow repositioning chapters within the book: app/views/chapters/list.rhtml: <h1><%= @book.name %> Contents:</h1> <ol> <% for chapter in @chapters %> <li> <%= chapter.name %> <i>[ move: <% unless chapter.first? %> <%= link_to "up", { :action => "move", :method => "move_higher", :id => params["id"], :ch_id => chapter.id } %> <%= link_to "top", { :action => "move", :method => "move_to_top", :id => params["id"], :ch_id => chapter.id } %> <% end %> <% unless chapter.last? %> <%= link_to "down", { :action => "move", :method => "move_lower", :id => params["id"], :ch_id => chapter.id } %> <%= link_to "bottom", { :action => "move", :method => "move_to_bottom", :id => params["id"], :ch_id => chapter.id } %> <% end %> ]</i> </li> <% end %> </ol> The list method of the Chapters Controller loads the data to be displayed in the view. The move method handles the repositioning actions; it is invoked when the user clicks on one of the up, down, top, or bottom links. app/controllers/chapters_controller.rb: class ChaptersController < ApplicationController def list @book = Book.find(params[:id]) @chapters = Chapter.find(:all, :conditions => ["book_id = %d", params[:id]], :order => "position") end def move if ["move_lower","move_higher","move_to_top", "move_to_bottom"].include?(params[:method]) \ and params[:ch_id] =~ /^\d+$/ Chapter.find(params[:ch_id]).send(params[:method]) end redirect_to(:action => "list", :id => params[:id]) end end DiscussionThe solution enables you to sort and reorder chapter objects in a list. The first step is to set up a one-to-many relationship between Books and Chapters. In this case, the has_many class method is passed the additional :order argument, which specifies that chapters are to be ordered by the position column of the chapters table. The Chapter model calls the acts_as_list method, which gives Chapter instances a set of methods to inspect or adjust their position relative to each other. The :scope option specifies that chapters are to be ordered by book, which means that if you were to add another book (with its own chapters) to the database, the ordering of those new chapters would be independent of any other chapters in the table. The view displays the ordered list of chapters, each with its own links to allow the user to rearrange the list. The up link, which appears on all but the first chapter, is generated with a call to link_to, and invokes the move action of the Chapters Controller. move calls eval on a string, which then gets executed as Ruby code. The string being passed to eval interpolates :ch_id and :method from the argument list of move. As a result of this call to eval, a chapter object is returned, and one of its movement commands is executed. Next, the request is redirected to the updated chapter listing. Figure 3-8 shows a sortable list of chapters from the solution. Figure 3-8. A sortable list of chapters using acts_as_list![]() Because move uses eval on user-supplied parameters, some sanity checking is performed to make sure that potentially malicious code won't be evaluated. The following instance methods become available to objects of a model that has been declared to act as a list:
See Also |