In my experience, really great software is so consistent that when you're trying to do something you've never done before, you can guess at how it's done and at least half of the time, it just works. For the most part, Ruby on Rails is like that. There are best practices baked into the framework for processes I never even realized you could have best practices for, like the unpleasant but necessary task of writing upgrade scripts. Through Rails, I've learned CS concepts like optimistic locking that were entirely new to me, and used them effectively after an hour's study and a dozen lines of code.
So it's disappointing and almost a little surprising when I encounter a feature of the Rails framework that doesn't just work automatically. acts_as_list
is such a feature, requiring days of reading source files and experimenting with logfile to figure out the series of magical incantations needed to make the darn thing work.
The theory behind acts_as_list
is pretty simple. You add an integer position
column to a database table, and add acts_as_list
to the model class it maps to. At that point, anytime you use the object in a collection, the position column gets set with the sort order of that object relative to everything else in the collection. List order is even scoped: a child table can be ordered within the context of its parent table by adding :scope => :parent_model_name
to the model declaration.
In practice, however, there are some problems I ran into which I wasn't expecting. Some of them are well documented in Agile Web Development with Rails, but some of them required a great deal of research to solve.
List items appear in order of record creation, not in order of :position
If an ImageSet has_many TitledImages, and TitledImage acts_as_list over the scope of an ImageSet, you'd expect ImageSet.titled_images to return a collection in the order that's set in the position
column, right? This won't happen unless you modify the parent class definition (ImageSet
, in this case) to specify an order column on your has_many
declaration:
has_many :titled_images, :order => :position
Pagination loses list order
Having fixed this problem, if you page through a list of items, you'll discover that the items once again appear in order of record creation, ignoring the value set in position
. Fixing this requires you to manually specify the order for paged items using the :order
option to paginate
:
@image_pages, @titled_images = paginate(:titled_image,
{:per_page => 10,
:conditions => conditions,
:order => 'position' })
Adjusting list order doesn't update objects in memory
Okay, this one took me the most time to figure out. acts_as_list
has nothing to do with the order of your collection. Using array operators to move elements around in the collection returned by ImageSet.titled_image
does absolutely nothing to the position
column.
Worse yet, using the acts_as_list
position modifiers like insert_at
will not affect the objects in memory. So if you've been working with a collection, then call an acts_as_list
method that affects its position, saving the elements that of collection will overwrite the position with old values. The position manipulation operators are designed to minimize SQL statements executed: among other side-effects, they circumvent optimistic locking. You must reload your collections after doing any list manipulation.
Moving list items from one parent object to another doesn't reorder their positions
Because acts_as_list
pays little attention to order within a collection, removing an item from one parent and adding it to another requires explicit updates to the position
attribute. You should probably use remove_from_list
to zero out the position before you transfer items from one parent to another, but since this reshuffles position columns for all other items in the list, I'd be cautious about doing this within a loop. Since I was collating items from two different lists into a third, I just manually set the position:
0.upto(max_size-1) do |i| # append the left element here if i < left_size new_set.titled_images << left_set.titled_images[i] end # append the right element if i < right_size new_set.titled_images << right_set.titled_images[i] end end # this has no effect on acts as list unless I do it manually 1.upto(new_set.titled_images.size) do |i| new_set.titled_images[i-1].position=i end
In my opinion, acts_as_list
is still worth using — in fact, I'm about to work with its reordering functionality a lot. But I won't be surprised if I find myself experimenting with logfiles again.
Anonymous says
Man, I’m glad I found this — thanks!
iFactoryOutlet says
this is true, plugins that help with collections have some unexpected side effects when they aren’t updating the database when you expect it to. I’ve resorted to doing a collection reload whenever I make a change to the list, to make sure I’m looking at the freshest copy from the database. Not the most elegant method I know. I should probably dig more into the source and figure out a better way.
Henry
Rain Hats
Melissa says
looks like you need to install a plugin to use it these days:
ruby scriptplugin install git://github.com/rails/acts_as_list.git
Thomas Stalcup Jr (TJ) says
wow thanks melissa, that helped alot!