you have a Object, lets call it “Item” with a “Display Order” Field, (called display_order, a integer) with controls the order which item are displayed.
When the item is created you could have the field blank and expect the user to know the right place to put it, but a better default is to always have it add to the end of the collection of item’s. Another assumption here is that you can not use a static default value.
I also expect that display_order is required and maybe should be unique – to avoid them all being 1 🙂
Update : based on Loop’s comment (below) I would clarify that “Display Order” would be used when you don’t want to display the Item (or Page or …) by the “id” (which would be similar to creation order), or by Name (which makes it easy for us people to find things in a list).
Another use case would be in building a “To Do List” and having a unique and required numeric “Priority” setting; Adding new “To Do’s” to the bottom is one solution.
(if you have more use case suggests, please post in the comments)
In all these use cases, it is implied that there is some way to change the display order. In the best scenario that would mean a user friendly way to re-order the list on a more global index view (for local values of global) using some ajax manipulation of the order values.
So, how to ensure the default is not blank and unique, but always at the end of the collection?
generally speaking, the solution is to find the current end value of the display_order, increment it by one and display that as the default value for a new item.
I going to present
3 4 5 working examples of code form “okay” to “good” to “better” all tested in Ruby in Rails 2.3.5 and Ruby 1.8.x
a) In the Fat Controller :
This is a standard generated “new” routine save for line 8 which calls a routine which :
1) uses the ActiveRecord Calculations .maximum method
2) sets the temp value to 0 if that is nil
3) otherwise increments the value by 1
and sets the display_order field in the newly created item object.
This works and is okay, except that, as alluded to in the section title, it makes for a Fat Controller!
Best practice calls for “Thin Controller, Fat Model” where controller only connects data with the view, and the model populates the values. It also increases the readability and testability of the code.
Check out Jamis Buck’s classic post from 2006 Skinny Controller, Fat Model, and a more recent – 2010 – post Rails Best Practices 1: Fat Model – Skinny Controller which talks about moving making the controller skinny.
b) In the Fat Model, the Local Way:
A standard rails model, with display_order being required and unique, except in the addition of a ActiveRecord Callback method after_initialize (see a a great description is in the wiki guide 10.4 after_initialize and after_find).
In the after_initialize, line 6 protects the values of any existing display_order values in the Item, so that default is only set on new Items. The other lines of the after_initialize do the same as the Fat controller version.
This works pretty well, for the simple case where you have only a little bit of edit activity and a small number of people or process’s adding Items. But it won’t scale. Image what would happen if 2 or more Items where created before any new item was saved! They would all have a display_order of X, and any items saved after the first item would get a validation error “Display Order not unique” though no fault of their own.
Lets look at how to save this…
c) In the Fat Model, the Server Way:
What’s different here is the use of a global variable $item_display_order_max_value to store a server level value which is initially tested and if it exists then incremented and used, and only if that is found to be nil then an inquiry is done to the database. This will have better performance because their is only a one ActiveRecord Calculations .maximum method call (which one be significant if the db table items was huge) on the first time a Item is created (and could be done on application start).
There are 2 issues to be aware of :
1) This implementation is only good when the application is running on one machine. if you have 2 or more instances running your application layer than you need to look at storing the value where they all can get at it. probably a shared database. (this this a use case for a NonSQL DB?). So this scales better but not unlimited.
2) It is very easy for non sequential values to get saved in display_order, if new item objects are created but some are not saved (abandoned). So it would be nice to expire the store globe variable after some time of non activity. The ‘best’ way to do this might be to store the date-time of the last update of $item_display_order_max_value and if this is older that X (say 10 minutes) than reset the $item_display_order_max_value with .maximum method. Non sequential values can also happen when items are destroyed / deleted. If this is a issue, it would be best to run a reordering routine in the background to lower values to fill the gaps when their is no edit or creation activity on items.
I’ve got a revised version of the code :
Which incorporates several comments and learnings :
a) rather than self.display_order.nil? I’m using self.new_record? , as per a suggestion.
b) rather than doing Item.maximum(‘display_order’), I’m using self.class to get (in this case) ‘Item’ which means one less thing to change and one set closer to a Helper.
c) I’m tracking the dateTime of the last max_value set and discarding it (and going back to the db .maximum method) if it’s older than X seconds. The length of this persistence is depended on the activity, the complexity of the form, and the time to compute .maximum.
d) I simplified the code making it (hopefully) clearer with less duplication.
Another, final revision, with lots of AutoMagic :
What is going on here is I’ve remove all the hard coding that can be removed. The global variables for the values and the dateTime are now a global hashes with the class name being the key.
The only thing you might have to customize is the field name you want to do the initialize max default, be that “display_order” or “sequence_order” or “priority_order”, which is used in the self.class.maximum method call.
Otherwise you can just copy it into the model and step back!!
the only other odd thing going on here is the lines
$max_initialize_value = Hash.new() if !(defined? $max_initialize_value)
$max_initialize__value_dateTime = Hash.new() if !(defined? $max_initialize__value_dateTime)
which create the 2 global hashes, if they have never been defined before.
Next stop : Gem-Land? (please)
d) as a Active Model Helper
okay I haven’t done this yet 🙂 but I would like to implement the “In the Fat Model, the Server Way” after_initialize as a plugin so that doing something like :
which would be really automagical.
I have been pointed to the default_value_for plugin which ‘allows one to define default values for ActiveRecord models in a declarative manner’, which adds static default values to the model. Worthy of further investigation. If I do make this a plugin, I expect it will have to be as a overloading (using ‘special’) of the initialize, given the special case that after_initialize is (you can not register after_initialize or after_find using macro-style class methods), so it might not be possible – or easy for values of easy where Ian still has some hair 🙂
I agree on the fact that moving this functionality to the model is a good thing. Only…
Why not use the default id as sort order? Am I missing something?
>> What if you want the new item to be the first or second item? One of the uses for this is for pages in a CMS. calling it item might have been a mistake on my part.
Why not use a callback method instead ?