LRBlog

Logical Reality Design: Web Design and Software Development

Rails 2.1 gotcha: don’t name an ActiveRecord field ‘changes’ !

July 10, 2008

In one of my Rails projects, I maintain a user-visible log of updates to the database by recording entries into a table that looks like this (from schema.rb):

create_table "log_entries", :force => true do |t|
t.integer "user_id", :limit => 11
t.datetime "created_at"
t.datetime "updated_at"
t.string "item_type"
t.integer "source_id", :limit => 11
t.string "table"
t.string "action"
t.text "changes"
end

Whenever a record is created, updated, or deleted, I create and save an instance of LogEntry, containing for example {:table => 'task', :action => 'update'} and in the 'changes' column I save a serialized hash showing which attributes of the task object changed before and after save. In addition, I save which logged-in user made these changes, and when.

This is convenient, it gives my client a log that's much more user-accessible and allows them to easily back-trace who did what to the database and when, which is important for their process and certain certifications.

When I upgraded the project to Rails 2.1 a couple of weeks ago, most of the code still worked fine, but the hashes showing the object changes were no longer showing up in views of the log. The culprit turned out to be Rails 2.1's new dirty feature. Why? Because it adds the method 'changes' to ActiveRecord::Base. This is a great new feature that lets you know what has changed to an ActiveRecord object since you loaded it from the database, with all kinds of benefits like doing more selective updates, or not writing to the database at all if no changes have occurred, thus reducing system load if you call save! a lot.

Unfortunately, the new method 'changes' was obscuring my attribute 'changes' in some circumstances, and at the very least confusing the heck out of me, the programmer.

The solution, of course, is not to name your fields anything that corresponds to any Ruby core method or any method of ActiveRecord::Base. I fixed my problem with a migration to rename the column 'details':

def self.up
rename_column :log_entries, :changes, :details
end

def self.down
rename_column :log_entries, :details, :changes
end

Site launch: CordobaSzekely.com

July 10, 2008

This morning we launched a refreshed site and an all-new online store for one of my clients, CordobaSzekely Productions.

Robert Cordoba and Deborah Székely are dance instructions and multiple-time national champions at the U.S. Open West Coast Swing Championships. They teach West Coast Swing and other dances all around the US and the world, and market a line of dance instructional DVDs.

Their former site had gotten a bit long in the tooth, so this was a just a quick refresher, keeping the former graphics design but bringing the HTML and CSS up to contemporary standards. Their online store was completely reimplemented using the open-source Zen Cart, where formerly they'd used the proprietary (and often difficult to use) Miva Merchant.

Getting <select> options in the right order

June 23, 2008

Sometimes you may want to generate a selector with a fixed set of options.   In one recent task of mine, we needed a selector for an integer 1 through 10, but the client wanted them labeled also with text to identify how the numbers mapped to the words "High" "Medium" and "Low". (We were selecting the priority level of a task in a project management application). Essentially, I want to generate this HTML output:

These are fixed name/value pairs, so it made sense to me to store them as a constant hash in my Task model:

Hash in Task.rb


PRIORITY_OPTIONS = { "1 - High" => "1", "2 - High" => "2", "3 - High" => "3",
"4 - Med" => "4", "5 - Med" => "5", "6 - Med" => "6", "7 - Med" => "7",
"8 - Low" => "8", "9 - Low" => "9", "10 - Low" => "10" }

However, I was rather dismayed to discover that this didn't produce the results I wanted, because Ruby hashes (in Ruby 1.8.6) do not preserve order. This is what came out:

Badly ordered priority selector

That's not what I wanted! I knew that select will also take arrays, but of course I needed separate name/value pairs, which I can't get with just straight arrays. I spent a while playing around with OrderedHash, which exists in Rails but is essentially undocumented and, as it turns out, does not support any useful functions of Hash like merge! and insert! that might make it easy to construct my list of options.

The Fix: Array of Arrays, or Array of Hashes

The documentation is not entirely clear on this, but if you send an array of 2-element arrays to select, rails will use the two elements of each inner array as if they were key and value pairs, and because the entire structure is an array it will preserve order. So, to get the results I wanted, I just need to change my constant to this:

PRIORITY_OPTIONS = [ ["1 - High", 1], ["2 - High", 2], ["3 - High", 3],
["4 - Med", 4], ["5 - Med", 5], ["6 - Med", 6], ["7 - Med", 7],
["8 - Low", 8], ["9 - Low", 9], ["10 - Low", 10] ]

And I get the result I was looking for :

Correctly ordered select options.

As it turns out, the way ActionView processes the options is fairly general: if you pass it any enumerable object, it will iterate that object, and for each element will check to see if that element supports the methods :first and :last (and isn't a string). If so, it will generate an option with the text set to element.first and the value set to element.last. If it was a string, or didn't support first and last, both the text and value of the option are set to the element itself.

Testing it

Here's a handy function I use for testing it the presence of a selector. You pass it the name of your selector, the name of hash or array of options (in the any format supported by select), and optionally the value of an item that should be pre-selected, and it will assert the existence of each of those things. If you need to handle selectors with multiple selections, you can just wrap the last assertion in a loop.

Drop this in test/test_helper.rb

# Assert existence of form select input
# the

#

# or as an array like this:
# [ 'foo', 'bar' ] will be asserted to match
#

#

def assert_select_input(name, option_values, options={})
attributes = { :name => name }

# first assert that the select tag exists
selector = { :tag => "select", :attributes => attributes }
assert_tag selector

option_values.each do | opt |
if !opt.is_a?(String) and opt.respond_to?(:first) and opt.respond_to?(:last)
assert_tag({:tag => "option", :attributes => { :value => opt.last },
:parent => selector, :content => opt.first })
else
assert_tag({:tag => "option", :attributes => { :value => opt },
:parent => selector, :content => opt })
end
end

# check for the pre-selected option, if any
if options[:selected]
assert_tag :tag => "option", :attributes => {:selected => 'selected',
:value => options[:selected] }
end
end

As an example of how to use it, here's my method for testing the task priority selector pictured above:

# asserts a