Charles Stross on Spam

A great quotation from Charles Stross's book Toast about spam and spammers:

Something the junk advertisers don't seem to understand: we live in an information-supersaturated world. If I don't want to buy something, no amount of shouting or propagandizing will budge me; all it will do is get me annoyed. On the other hand, if I have a need for your product, I can seek it out in an eyeblink.

-- from the short story, TOAST: A Con Report, by Charles Stross

Rails Tip: Accessible Field Errors in Tight Spaces

Problem

I am currently working on a project to migrate a bunch of one-off promotional sites onto a single platform that can serve them all. Pretty basic user experience: a consumer performs a search for a product, clicks on an advertisement, is brought to one of these promotional sites, and, if interested, the consumer fills out a form and is taken off-site to perform the necessary action to complete the promotion. One problem I inherited is an image heavy layout, completed by a third-party agency, which allows no room for form field error messages. Current site behavior is to report errors by highlighting the invalid fields with a red border:

This can be pretty uninformative if the consumer enters data, but the data fails validation. What I want is to be able to give consumers a way to find out why the field is invalid, while not breaking the third-party developed layout that doesn’t supply room for error messages.

Solution

Instead of highlighting the field that is invalid, I want to highlight the field’s label; and within that label, I want to apply a title tag, that if the consumer hovers over, will give them a description of why the field is invalid.

Implementation

In order to implement the solution, we need to build a custom FormBuilder that will auto-generate our label and form field for us, applying a title tag with error messages when applicable.

Filename: app/helpers/tagged_builder.rb

class TaggedBuilder < ActionView::Helpers::FormBuilder
  def self.create_tagged_field(method_name)
    define_method(method_name) do |label, *args|
      errors = object.errors.on(label.to_sym)
      klass = errors.blank? ? nil : 'errorLabel'
      msg = label.to_s.humanize
      unless errors.blank?
        msg += ' ' + ((errors.class == Array) ? errors.join(' and ') : errors)
      end
      
      @template.content_tag("div", @template.content_tag("label", "#{label.to_s.humanize}:", :for => "#{@object_name}_#{label}", :class => klass, :title => msg) + super)
    end
  end
  
  field_helpers.each do |name|
    create_tagged_field(name) unless ('hidden_field' == name)
  end
end

or each of the predefined field_helpers (except hidden fields) we want to process our custom create_tagged_field method to build a containing div and our labelwith optional error messages, before we call super to build the form field.

In generating the error message string, we check to see if there are any error messages recorded on the field we are building: object.errors.on(label.to_sym). If we find errors, (errors.blank? is false), we assign an errorLabel class to the label we are creating. errors.on will return a string if one error was found, or an array of error messages if more than one were found. To format our error messages appropriately for a title attribute, we need to perform a join if we have an array of errors (errors.join(' and ')), otherwise we can just use the error message string.

When creating a form in our view, we need to tell form_for to use the custom builder we just wrote:

Filename: app/views/your_class/_form.html.erb

<% form_for(@model, :builder => TaggedBuilder) do |f| %>
  <%= f.text_field :my_field %>
  ...
<%- end -%>

This will now generate our labels and form fields appropriately, but another thing we should consider doing is overwriting the default error handling behavior of Rails. By default, when a field has an error, Rails will surround the form field in a div with a class of fieldWithErrors — even with our custom form builder. What we are going to do is keep the fieldWithErrors class, but move it into the field directly by overwriting ActionView::Base.field_error_proc:

Filename: config/environment.rb

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  error_class = "fieldWithErrors"
  if html_tag =~ /<(input|textarea|select)[^>]+class=/
    class_attribute = html_tag =~ /class=['"]/
    html_tag.insert(class_attribute + 7, "#{error_class} ")
  elsif html_tag =~ /<(input|textarea|select)/
    first_whitespace = html_tag =~ /\s/
    html_tag[first_whitespace] = " class='#{error_class}' "
  end
  html_tag
end

This procedure keeps any user defined classes and appends the fieldWithErrors class when necessary, without wrapping the form field in a div.

Finally, we add a little style to make the errors stand out more:

label.errorLabel { color: #F00; cursor: help; }

What we end up with is a nice compact form that still informs consumers of validation errors:

Skydiving This Saturday

Woo! I've thought about skydiving for quite some time now and I finally have the option. My friend Rob wanted to surprise his girlfriend Agnes for her 25th birthday by taking her skydiving a couple of weeks ago, but with crappy weather and Agnes being sick, plans were delayed. The skydiving trip has been rescheduled, to this weekend!

On Saturday, May 10th, 2008 at 10:30 a.m., Stephanie and I will be joining Rob, Agnes, and a few other friends at Skydive Chicago for our first tandem dive!

Wedding Photos

All of our wedding tasks are officially over! Last night we drove out to Lisle, IL and picked up our Wedding Album from Timothy Whaley & Associates, thus closing the book on our wedding. No more appointments to worry about, no more payments to make, nothing!

As part of picking up our album, we also got possession of our proofs, which I have promptly placed in my Flickr Wedding Set for all to see. Anyone who missed the ordering deadline from Timothy Whaley (or anyone who wants more pictures) can now grab the photos straight from Flickr and have them printed anywhere.

Custom Subversion Rake Tasks, Part 1

One of the most tedious aspects of working with Ruby on Rails with Subversion is setting up the initial project to exclude the right files and set up all of the directorypropsets. Having fallen into the role of checking in the initial Rails project for a new platform I have been asked to help build at work, I thought it was about time to get the Rails Subversion Setup down to 3 steps. This post is not going to talk about how to setup and run Subversion, there are plenty of articles out there on that. So, once your project is setup in svnadmin, check out the empty trunk directory, and from there:

  1. Run rails . from inside trunk to have Ruby on Rails build a blank project
  2. Copy svn.rake into the lib/tasks directory and run rake svn:setup
  3. There is no step 3

That’s it! You are now be all set to begin development on your application and are one step closer to being bought out by !

svn rake:setup

You could just believe that the above process works by magic, but let’s take a step-by-step look at what the svn:setup rake task actually does.

if File.exists? 'config/database.yml'
  move 'config/database.yml', 'config/database.example.yml'
end

Pretty straight forward, we don’t want to store our database.yml file in subversion because, let’s face it, access to your production database should probably be safe-guarded just a little bit. We first make sure the default config/database.yml exists, then rename it to what it is, an example file: config/database.example.yml.

files = Dir.glob('log/*') + Dir.glob('tmp/*') + Dir.glob('db/*.sqlite3')
files.each { |file| remove_entry file }

We don’t want to store log files, temporary files, or development databases in subversion. Dir.glob returns a list of filenames (paths) that match a given pattern. Combine all the paths found into one list and use remove_entry to delete each file or directory.

mkdir_p ['doc/api', 'doc/app']

f you want to have Ruby on Rails documentation (rake doc:rails) or your application’s documentation (rake doc:app) on your local machine, you shouldn’t pollute subversion with it. Let’s make the doc/api and doc/app directories now, so we can setup subversion rules later to prevent the documentation from being added to the repository.

Rake::Task["rails:freeze:gems"].invoke
Rake::Task["rails:update"].invoke

You should freeze rails in your application to ensure you are developing against the same version of Rails no matter where the code is checked out or deployed. SeeFreezing Your Rails Application for more information.

Rake::Task["svn:add"].invoke
`svn commit -m "Initial checkin with frozen rails"`

We then invoke another helpful Subversion Rake task I wrote (to be explained in Part II) to mark all of the files we have in trunk to be added to the subversion repository and make a system call to actually make the commit.

`svn propset svn:ignore "database.yml" config/`
`svn propset svn:ignore "*" log/`
`svn propset svn:ignore "*" tmp/`
`svn propset svn:ignore "*" doc/app/`
`svn propset svn:ignore "*" doc/api/`
`svn propset svn:ignore "*.sqlite3" db/`

Finally, we set up our svn propsets so subversion will ignore (svn:ignore) all unnecessary files:

  • config/database.yml – ignore local database settings
  • log/* – ignore all log files
  • tmp/* – ignore all temporary files
  • doc/app/* – ignore all application documentation
  • doc/api/* – ignore all Rails documentation
  • db/*.sqlite3 – ignore all local SQLite database files (development, test, etc.)
`svn commit -m "Initial svn:ignore list - config/database.yml log/* tmp/* doc/app/* doc/api/* db/*.sqlite3"`

And commit the settings to our subversion repository.

Check out Custom Subversion Rake Tasks, Part II for an explanation of the rest of the custom Subversion Rake Tasks (svn:add, svn:delete, svn:update, svn:commit, andsvn:status).