Wednesday, 31 December 2008

Link: A signature cadence

Too many websites (and applications) are soul-less, corporate entities that treat you like just another number.

Rands' article: A signature cadence shows how a human tone can make a big difference in how we view a site. Even tiny details like the copyright wording can help make an overall impression that you are actually a bunch of real people. I think we need more of that.

Tuesday, 30 December 2008

Squidoo and ssh tunneling

I've found squidoo - a site where you can set up pages called lenses that help direct people to all the relevant information about a specific topic.

I love it!

I've only just begun, but I think I'll slowly add pages on my favourite topics and see where it goes.

ssh tunnels R Us

To start with, I put up a page on ssh tunneling.

For years I've used ssh to login to a remote linux box to get work done. But recently I had to get through to the http server on my work machine - which was hidden behind a firewall. The best way to do that, it seems, is to set up an ssh tunnel and just port-forward the http port to my local machine!

In the process I found several useful (and less useful) articles on the subject, and it took me ages to crawl through each one to figure out which was which. It seems "ssh tunneling" means different things to different people...

So where does squidoo feature in all this?

In the squidoo lens, I gave an overview of ssh and tunneling - and then listed each of the articles with a brief description of the contents. In this way, the squidoo page acts as a differentiator for any people searching for the same thing in future. Hopefully it'll cut down the time people need to spend scanning all the possible google results they can find. They can zero in on an answer all the more quickly.

[Edit...] More squidoo...

So I've now gone and added a few more pages on my other hobbies.

For starters, I quite like brewing mead - which is a wine made from honey. I've been doing it for about 15 years now, and you can read more about it in my lens on mead, or you can jump straight to my (long) howto lens that will teach you how to make mead in three weekends.

Alternatively you can try your hand at home preserving with vacola - an ingenious system of bottling that creates a vacuum seal in the bottle to hold on the lid.

Stay tuned for more fun stuff as I go along.

Thursday, 25 December 2008

Link: 13 coding nightmares to avoid

Here is a great list of bad RoR practices - make sure you read the comments as there are some interesting exceptions raised.

Merry Christmas!

So I've been pretty quiet for the past month. That's because I'm running around like a mad thing getting set to make a big move to the UK. Won't be for another month yet, but it's consuming all my spare time.

Anyway, today's been quiet enough to sit down and actually write something so - merry christmas everybody!

Saturday, 29 November 2008

I wrote a novel - w00t!

Woo hoo - much celebrating as I have successfully finished 50,000 words for NaNoWriMo! You can see my novel info here - though not the novel itself, that's still only a first draft!

Now to party for a while. :)

Wednesday, 5 November 2008

KioqKioqKioq OR fixing mutt's base64 problem

So I've occasionally been getting *really* useful email messages such as:

KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq
KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq
KgpUSElTIElTIEFOIEFVVE9NQVRFRCBFTUFJTCAtIFBMRUFTRSBETyBOT1Qg
UkVQTFkuCioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq
KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq
KioqKioqKioKCgpEZWFyIE5ldyBNb25leWJvb2tlcnMgQ3VzdG9tZXIsCgpZ
b3UgaGF2ZSBjaG9zZW4gbWJfdGVzdF9jb21wYW55QHRhcnluZWFzdC5jb20g
YXMgdGhlIGVtYWlsIGFkZHJlc3MgZm9yIHlvdXIgbmV3IE1vbmV5Ym9va2Vy
cyBhY2NvdW50LiAKClBsZWFzZSB2ZXJpZnkgdGhpcyBlbWFpbCBhZGRyZXNz

Not having the slightest clue as to what was causing it - I'd simply return them to the sender and ask for help... sadly the senders generally didn't know what it was either.

It turns out that the sender's setup is incorrectly labelling these emails as 8bit... when it's actually been encoded in base64. To fix it:

  1. Save the email to its own new folder (just so there's only one email in the file to edit).
  2. Open it up in a plain text editor (eg vim)
  3. Update the header line that reads:
    Content-Transfer-Encoding: 8bit to
    Content-Transfer-Encoding: base64 - save and close
  4. Navigate to the folder in mutt and read away happily.

Sunday, 2 November 2008

National Novel Writing Month

It's November - and that means it's time to whip out a pen and start a frenzy of writing in the hopes of finishing up a new masterpiece... or at least 50K of whatever comes to mind during this month of madness.

It's National Novel Writing Month (or NaNoWriMo for short). The site was slashdotted for the first day and a half, but the servers seem to be taking the load now - which means we have a chance to actually update our wordcounts and read the forums.

NaNoWriMo is a personal challenge to start writing a novel on November 1st and aim to have 50,000 words by midnight on November 30th. Last year there were over 100,000 participants - of which 15,000 passed the 50K mark by month's-end. I was one of those 15K :)

Here's a link to my author page so you can see how much I've done so far this year. I'll also add a wordcount widget onto the blog for the duration of the month (should be there in the left-hand column).

It's heaps of fun and it's open to anyone. So, if you're up to a unique kind of personal challenge, join us in our month of writing. Or just come along and see what all the fuss is about!

Wednesday, 29 October 2008

Rails a little over-protective?

Was busy developing and minding my own business, when suddenly my functional tests all stopped working giving me this error:

ActionController::InvalidAuthenticityToken: No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store).

Now we happen to have just switched on the use of the cookie store - and the actions all work just fine from the browser. Checking the forms even shows up the authenticity tokens etc... this only occurs during testing.

Googling gives a few rspec-based solutions, but very few are even possible for Test::Unit. I tried disabling config.action_controller.allow_forgery_protection - but that did a big nothing for me. So I bodgied up a quick-n-dirty workaround just for the test environment as per below.

Right now I still don't know what's causing the bug, but this gets me past it until I can figure out what's actually wrong. YMMV :)

  # See ActionController::RequestForgeryProtection for details
  if RAILS_ENV == 'test'
    # dodgy workaround for Rails 2.0 bug in functional tests - which don't
    # seem to use a cookie store properly. Reference to issue:
    # http://groups.google.com/group/railsspace/browse_thread/thread/fcdbfa4e65bf86de
    protect_from_forgery :secret => 'c1c6ebaee01fecc9aa9bc105d235b2c2'
  else
    # Uncomment the :secret if you're not using the cookie session store
    protect_from_forgery # :secret => 'c1c6ebaee01fecc9aa9bc105d235b2c2'
  end

Tuesday, 28 October 2008

Headhunters strike out again

A follow-on from my negative experience with headhunters. I've begun receiving exactly the sort of "job offers" that I was fearing. I've got offers (from all over the world, mind) asking me if I'd like to work as (for example) a junior developer on an embedded-kernel contract in Belgium... which'd be nice and useful if I were interested in that sort of thing instead of having only 6 months non-commercial experience (at uni) doing this stuff as a elective subject... or at least held a work-permit that allowed me to work in Belgium.

The end result makes it fairly obvious that their recruiters are just too damn lazy to do more than a quick search on the db... followed by spamming all the results. They've ripped out the db-friendly data (ie my naked skillset) and divorced it from the important stuff (ie what sort of job I'm actually interested in). Leaving me to be spammed by every idiot with a contract to push.

If I were a junior developer, desperate for any job I could get, I'd probably just have to suck it up... but I'm not - and they're alienating an in-demand developer with lots of experience. This doesn't make me feel like responding positively to any of their future offers... even if they do suit my requirements.

What gets me is that it'd be so easy to make this system work. Even if they had a few extra fields in their db for "what type of job is the candidate looking for", they could run these as a filter over their resultset and turn that spam into much-less-despised targetted advertising.

I'd even be interested (possibly even grateful) to receive such a service - instead of so annoyed that I've blogged about it twice.

Enumerable.average

Was looking around for a quickie function to do an average on an Enumerable/Array just like max and min - and found there isn't one. So here's a quick-n-dirty one that you can drop into config/environment.rb. Examples for use in the comments:

module Enumerable
  # returns a simple average of each item
  # If a block is passed - it will try to perform the operation on the item.
  # if the result is nil - it won't be counted towards the average.
  # Egs: 
  # [1,2,3].average == 2
  # ["a","ab","abc"].average &:length == 2
  # @purchases.average &:item_price == 10.45
  def average(&block)
    the_sum = 0
    total_count = 0
    self.each do |item|
      # either the actual item, or a method called on the item
      next_value = block_given? ? yield(item) : item
      unless next_value.nil?
        the_sum += next_value
        total_count += 1
      end
    end
    return nil unless total_count > 0
    the_sum / total_count
  end
end

I used it for displaying neat averages in the view thus:

    <p>Averages:
    <br />Number of items: <%= @purchases.average &:num_items -%>
    <br />Unit price: <%= @purchases.average &:price -%>
    <br />Total price: <%= @purchases.average {|p| p.num_items * p.price } -%> </p>

Thursday, 23 October 2008

That doesn't help me, it helps you

I just got off the phone with Australia Post and I'm not happy. Even though they were very polite to me, they failed to provide a good customer service experience. This issue requires a bit of backstory.

While in Melbourne last weekend, I'd bought two half-cases of wine and paid for them to be delivered. I have no car, so I left explicit instructions for the parcels to be left on the back doorstep if I was not at home.

I got one of the little blue cards in the post yesterday, telling me that they tried to deliver one of the parcels to me in the 1 hour when I was out of the house getting some lunch. I figured that the winery must have forgotten to put the "in case not at home" message on the parcel, so today I decided to just fetch it from the post office.

I damaged my knees fencing, a couple of years ago, so walking eight blocks while carrying a heavy box is painful for me. My arms and knees both ache, now, but I got the parcel home... only to find a second little blue card in the mailbox.

The parcel from yesterday clearly has the "in case not at home" instructions printed on the outside, so I figured I'd call up Australia Post and see if they could actually do what I paid them to - which is to deliver my second parcel to my house.

I called them up and waded through their long telephone menu-system to get to a real human being. The man at the other end seemed kind enough, but the only option available to him was to allow me to lodge a complaint against the contractor who failed to follow the given instructions.

I don't really want to lodge a complaint - I just wanted my parcel delivered - which is what I paid for.

I asked the man if he could find anyone that would be able to help me get what I wanted. He went back to his supervisor, leaving me on hold, and returned to tell me that the only way I could get it redelivered was to call up the people that I originally bought it from (some winery out in the Yarra valley - I don't know which one as the parcel is still sitting in the aussie post office), and to get *them* to call aussie post and figure out if their contract supports "redelivery-on-failure"...

Good grief! From my POV it's pretty simple:

  1. I paid for delivery to my home
  2. Aussie Post stuffed it up
  3. Aussie Post should find a way to get it right

Instead of finding a way to get my parcel to me, they kept insisting that they were helping me by starting up a complaints process and making sure it "never happens again". If you want to reprimand your contractor, that's your business. It will help you improve your own processes over time, and no doubt his non-instruction-following is the weak link that broke in this particular chain of events...

But doing this isn't going to help me. Getting my parcel to me helps me - that's all I want, and Aussie Post wasn't willing ot help in that regard. I was a customer on the line right then and there with a problem that Australia Post caused - and wasn't able to get any resolution that actually solved that problem.

I was left with the feeling that Australia Post wasn't willing take responsibility for this mistake and do something about it. Instead they wanted to shift the blame to the contractor, and then claim that responsibility was out of their hands.

This reeks of bad customer service.

If you want to leave a good impression with your customers, follow one of the basic rules of human conduct: if you stuff up, take responsibility and try to fix it. Whatever you do, don't just give a reassurance that it won't happen again... we won't believe you. After all, you've just proven that you can't be trusted. At least if you try to find a way to give me what you promised in the first place - it'll go a long way towards reassuring me that you can act in good faith.

Tuesday, 14 October 2008

soap4r + rails can't find SOAP::Mapping::EncodedRegistry

Joining a new project, I had to go through the usual setting-up rigmarole - which means finding several new things to break/configure/reinstall. One of which was to get soap4r to run.

I'd run gem install soap4r just fine, but running rake was giving me the error below:

rake aborted!
uninitialized constant SOAP::Mapping::EncodedRegistry

Scrounging around gave several solutions that didn't work, mainly using two of the three lines below. I required all three of the following before it ran for me.

# Add these lines to config/environment.rb just before the Rails initializer code
require 'rubygems'
gem 'soap4r'
require 'soap/mapping'

Tuesday, 7 October 2008

Words and pictures

Ok, this is a neat toy: Wordle is a site that runs a java applet that takes some text (or a URL for a feed) and make a pretty word-picture... like the one below for this blog.

Hmmm... I wonder if I can use it to improve the editing of my blogposts. Looks like I use "going" far too much!;)

Sunday, 28 September 2008

Headhunters and a dearth of trust

What is it with some headhunters?
I had a phonecall from a headhunter the other day that really rubbed my fur up the wrong way.

First, they wanted my CV in Word format. I hate that. Quite apart from having to explain that I use Linux - which frequently leads in to the "what is an OS" discussion that I find so annoying coming from an IT recruiter. Yes I can use OpenOffice to create a Word doc, but it's a preference thing. I prefer to write my CV in PDF so I have more control over the formatting - because I've found that .doc format "helps" too much, and suffers from bit-rot.

But it's not like they can't read/print-out a copy of my CV if it's in PDF. What really gets me is that there are only two reasons for wanting my CV in Word. First is so they can copy/paste my skills/experience into their mammoth database - reducing me to a set of numbers... which I really don't like.

I recognise that might be a knock to their usual system, but I really don't like being just a number in an uncharted sea of applicants. It doesn't work for me to be recruited by a company that would view me as only so important as the number (and not the quality) of the years of experience I have.

Second is that they want to remove my contact information so that they are the only point of contact between me and the employer. This view is especially reinforced by headhunters that refuse to give you the prospective employer's name, industry, size or any other potentially-identifying details (or politely change subject when I ask, as this one did). How the hell am I supposed to decide if I'm interested in working for a company I know nothing about?

I should point out - I get a phonecall a week from various recruiters. I don't have time to go to an interview a week - so I have to decide, based on what you tell me, if you are worth a morning of my time.

Word to the wise: recruiters perform a valuable service - both to employers and employees. If you are good at what you do, then nobody will be unhappy about paying you for your services! The only recruiters that need to resort to cheap tactics like hiding names are the ones that cannot build a reputation for excellence on their own... thus if you flat-out refuse, then it instantly gets my suspicions up.

Yes, I'm sure you will get ocasionally burnt by employees and employers that are unscrupulous and try to screw you out of your fee. But are those the kind of clients you want to keep anyway?

Recruiting is a people business - it is outsourced HR and builds upon long-term relationships. Trust is important, and screwing around like this ruffles that layer of trust and just feels tacky.

Please don't do it.

Thursday, 25 September 2008

Rspec mocks too much

I've recently had to learn the other side of testing - ie rspec, due to my new contract having a test suite written in "the other testing suite" * ;)

Seems fairly straightforward so far, but there's one big worry I have: Rspec has a huge reliance upon mocks.

Now I completely understand (and endorse) the benefits of using mocks. It means you can independantly test a model/controller/whatever without it mattering if something happens to have broken in some other model/module/whatever. This is a Good Thing and allows for more accurate testing due to all the benefits of loose coupling. However, I get a bit worried if everything is mocked out and there are no integration-style tests at all.

For example, if I'm testing a view, and using rspec best practice I've mocked out all model objects and don't bother with the real thing. What happens if a model changes (say a column is removed that shows up on an index list)? The view specs all rely on a mock that explicitly states what is returned, so the view specs all still pass. But the index page will be broken for a user that actually uses the site, because the real object doesn't bahave that way anymore. The mocks and the model are out of synch. :P

In an infrequently-used area of the site, this sort of error may go undetected and slip through to release.

This worries me. I know I like to rely on my test suite to catch things like this wherever possible. Sure I generally do a manual click-through, but I know that this is unreliable especially WRT edge-cases.

So, am I needlessly worrying? Have I simply missed something about how rspec is supposed to be used? What's best practice in this situation?

*Which admittedly feels a lot like having to switch from vi to emacs... Sure I know that "the other one" works for lots of people, and I meant to get around to learning it someday...

Tuesday, 23 September 2008

Getting ffmpeg to work under ubuntu

I had a fair whack of trouble getting ffmpeg to compile on ubuntu. These are the steps I took that finally got it working for me.

Firstly - I'm going to assume you've downloaded and installed the appropriate libraries (codecs?) for the various audio/video formats that you are going to use (eg lame). I'm just going to cover making ffmpeg actually compile and run.

The usual set of steps will be

  • Downloading ffmpeg: svn checkout svn://svn.mplayerhq.hu/ffmpeg/trunk ffmpeg
  • cd ffmpeg
  • Configuring appropriately to your build. Eg: ./configure --enable-libmp3lame --disable-vhook --disable-mmx --enable-nonfree --enable-gpl --enable-shared --enable-libamr-nb --enable-libfaad --enable-libx264 --enable-encoder=x264 --prefix=/opt/local --extra-ldflags=-L/opt/local/lib --extra-cflags=-I/opt/local/include
  • make
  • sudo make install

If that all works for you, then brilliant, but for me it fell over while trying to make, telling me it couldn't find x264. For some reason it's necessary to add --enable-pthreads to make it work on ubuntu.

The next big hurdle for me was that a bug appears in the v4l2 library for ubuntu. It doesn’t properly load some of the basic linux types. This is apparently a well-known bug in some of the older linux headers, and will stall the compilation of ffmpeg with a whole slew of bogus error messages, that essentially boil down to the __u64 and __s64 types failing to be defined appropriately.

To make a temporary fix, you can open up asm/types.h (I used find to find it) and figure out where the asm types are located on your distro (for me they’re in asm-i386/types.h).

Now, copy the two lines that define __u64 and __s64 and paste them into:
/usr/include/linux/videodev2.h just above their first use to help define v4l2_std_id

Note - this is an ugly, quick hack, but it should get the compilation going again. For a more permanent solution - I'd suggest chatting to the ubuntu people... or maybe it's time for me to get around to upgrading from feisty ;)

That will generally get you compiled and installed, but I then fell over a run-time bug that boiled down to an error message: “cannot find libavdevice.so.52”. This problem had me stalled for weeks. Google returns a lot of talk about the problem, but very few solutions, but eventually I stumbled across the answer: you need to add a library path to your environment thus:
export LD_LIBRARY_PATH=/usr/local/lib/

Finally ffmpeg safely up and running on ubuntu. Yay!

Saturday, 20 September 2008

Snippet: MyModel.options_for_select

How many times have you written a quickie drop-down box to choose one instance of an associated model? Here's a quickie monkey-patch that will help. Put the following in environment.rb (or similar):

class ActiveRecord::Base
  # convenience method for selection boxes of this model
  # Assumes existance of a function "display_name" that will be a
  # displayable name for the select box.
  def self.options_for_select
    self.find(:all).map {|p| [p.display_name, p.id] }
  end
end

Make sure you have a function in your model named "display_name" that returns what you'd like to see in your drop-down, eg:

class Person < ActiveRecord::Base
  has_many :posts

  # convenience method for the person's name
  def display_name
    [first_name, last_name].compact.join(', ')
  end
end
class Post < ActiveRecord::Base
  belongs_to :person

  alias :display_name :title
end

Then use in a view (eg as follows):

<% form_for(@person) do |f| %>
<p>Pick a post: <%= f.select :post_id, Post.options_for_select -%><%= f.submit 'Go!' -%></p>
<% end %>

Thursday, 4 September 2008

Snippet: monkey-patching a gem

Need to make a quick monkey patch to a gem you're using? Follow the steps below:

  1. If you don't have a vendor gems directory: svn mkdir vendor/gems
  2. cd vendor/gems
  3. Unpack the gem into your vendor/gems directory with
    unpack gem <the_gem_name>
  4. Checkin the above changes so you have a clean version of the gem to start with
  5. Make your monkey patches and check them in
  6. Make sure vendor/gems is in the loadpath for gems

And you're done

Tuesday, 19 August 2008

Link: Why I don't have a startup yet

Great post called Why I don't have a startup yet. Snippet:

"I love what I do at Yahoo, and I care enough about what we create that I want to focus all my energy on creating value for users. It’s good practice. But as long as I work at this job, I won’t have enough left over at the end of the day to seriously invest in anything else."

But further to that - there's so much in the world to do. Doing work is part of that, but if it's the only thing in your life, then "All work and no play makes Taryn a dull girl". Sure, some coding is play - and I do love to code my own stuff too... but I like so many things, that if I focus my off-hours on coding, I'd lose sight of all the rest that life has to offer.

So sure, I choose to spend some time coding, but also a lot of time reading books while sipping great coffee, getting together with interesting bunches of people, travelling to far-flung places, writing up my travels or even writing fiction (however badly), taking photographs, attending medieval feasts and wars, making home-brew mead, or even just walking outside and enjoying the sun.

Life is an adventure... make the most of it.

Wednesday, 13 August 2008

Tact filters for geeks

I just stumbled into this great article. It described the difference between how geeks vs "normal" people filter their social discourse for tact. It makes a lot of sense, and explains my frustration with non-geeks not "talking straight"... as well as, no doubt, the shocked reactions of these same people when I do. Interesting reading.

Tuesday, 12 August 2008

Updating gem to 1.2.0 on Ubuntu

Like several others, I tried to gem update today and failed. It gave me weird error telling me it couldn't find the update gem in the repository:

%> sudo gem update --system
Updating RubyGems
Bulk updating Gem source index for: http://gems.rubyforge.org/
Updating rubygems-update
ERROR:  While executing gem ... (Gem::GemNotFoundException)
    could not find rubygems-update locally or in a repository

Googling gave me a fair few other links to people with the same issue. The main one seems to be listed under this ruby-forum topic which started as the release-notes for the Gem update to version 1.2.0. People that had problems updating to this gem listed their steps-to-reproduce in the replies below, and I tried all the suggestions.

sudo gem update --system was the original command I tried and it gave me the error as above. All the fora on the topic insisted that the workaround is to run: gem install rubygems-update -v 1.1.1, but this just gave the same error. I did note that most of these people were on various flavours of Mac, and the pleas for help from those on Ubuntu were drifting away into the sky unheard...

One forum user said he tried the above from his gem directory and that worked for him...
but not for me...

I even tried reinstalling ruby/rubygems via apt-get, but the former was latest version, and the latter doesn't exist...

I was about ready to nuke the site from orbit... and reinstall from scratch and went to the rubyforge page for rubygems. This lists the source code, but I also noticed it had individual "upgrade" gems available for download. This saved me from my previous scorched-earth plan, and eventually resulted in success. For future reference:

  1. copy rubygems-update-1.1.0.gem to a local directory
  2. then run: sudo gem install rubygems-update-1.1.0.gem
  3. repeat with rubygems-update-1.2.0.gem
  4. then run update_rubygems

Now it's all fine and sudo gem update --system happily fetches my new gems as before.

Thursday, 7 August 2008

Explaining anti-virus suckage to the tech-averse

Being the most IT-savvy member of my extended family, I'm often called upon for random tidbits of IT information. This is regardless of whether or not I'm actually likely to know anything about the subject at hand... because obviously I'm "into IT" so I should know how to network an ancient dot-matrix printer to an ageing pentium... (clue: it's probably not working for you because it's time to replace the hardware).

The one I dread most, however, is "why is my internet so slow"... the obvious candidate being "have you considered the possibility that you might have a virus?" - which is usually met with the same sort of scandalised reaction reserved for asking somebody whether they have a venereal disease.

Surely computer viruses are something that only happens to *other* people? After all, we're good, clean types that don't take part in *that* sort of behaviour... why would we have something so dirty as a virus? besides... which we always use a condom anti-virus software... so there's no possible way we could have anything right?

Depending on the tech-savviness of the audience I'll then launch into an explanation as to why anti-virus software doesn't always work... This generally goes down like a lead balloon. With the usual response being "but I have the latest <insert expensive AV software name here> and keep the signatures fresh up-to-the-minute!"

The problem being that I know this subject only enough to know that even with the latest virus signatures, it's not perfect. I don't know it well enough to really describe in laymans' terms *why*.

Today I came across an article on Coding Horror about how blacklists don't work. but it also neatly segue's into the reason why anti-virus software is always going to be one step behind the competition. I think it summarises the issue nicely - far better than I have ever been able to describe.

I know I could never give this to my grandma, but I think it's a step in the right direction - and I can probably point my partially-tech-savvy aunts and uncles in that direction from now on.

Tuesday, 29 July 2008

icon_button_to_remote

Following from my earlier post on making icon buttons, I needed to add AJAXy goodness to the edit actions on one of our index pages.... but I still wanted cute little icons to make them look pretty.

So I've updated the code to incorporate an icon_button_to_remote method. Note that icon_button_to has also been modified, but it still depends upon the get_icon_path and fixed_button_to methods from the previous post.

  # generates appropriate image-buttons, given an "icon type" and the usual
  # button options.
  def icon_button_to(icon_type, icon_label, form_opts = nil, button_opts = {})
    unless form_opts.blank?
      button_opts.merge!(:type => 'image', :src => get_icon_path(icon_type), 
                       :alt => icon_label, :title => icon_label)
      the_icon = fixed_button_to(icon_label, form_opts, button_opts)
    else
      # no form was passed in - we want a non-linked icon.
      the_icon = image_tag(get_icon_path(icon_type), :width => 16, :height => '16', 
                       :alt => icon_label, :title => icon_label)
    end
    content_tag 'div', the_icon, :class => 'icon_button'
  end
  # add a remote AJAX-linked icon button
  def icon_button_to_remote(icon_type, icon_label, remote_options = {}, button_options = {})
    link_to_remote(icon_button_to(icon_type, icon_label, nil, button_options), remote_options)
  end

The example would be for an index page with a list of Widgets. Each one has an "edit" button using the code below. The page also has a section for entering a new widget - which would be replaced by the "edit this widget" template when you click on the icon.

<!-- In the template at the top of the list -->
<%= w_path = new_widget_path() # url_for is slow
    icon_button_to_remote(
         :new, 'Add new widget', 
         :update => 'new_widget_div',
         :url => w_path,
         :method => :get,
         :html => {:href => w_path},
         :complete => "Element.hide('edit_widget_div'); 
                          Element.show('new_widget_div'); 
                          new Effect.Highlight('new_widget_div');"
        ) -%>

<!-- and next to each widget -->
<%= ew_path = edit_widget_path(@widget) # url_for is slow
    icon_button_to_remote(
         :edit, 'Edit this widget', 
         :update => 'edit_widget_div',
         :url => ew_path,
         :method => :get,
         :html => {:href => ew_path},
         :complete => "Element.hide('new_widget_div'); 
                          Element.show('edit_widget_div'); 
                          new Effect.Highlight('edit_widget_div');"
        ) -%>

<!-- Later in the template... -->
<div id="new_widget_div">New widget form would go here</div>

<div id="edit_widget_div" style="display:none;">Edit widget form will be populated here</div>

You also have to update your Controller code for the new and edit actions to accept js and respond with no layout - otherwise your div gets populated with the entire edit/new page :P

The best way is to move the meat of the page into a partial (which can also be loaded in the edit.rhtml template) callable from the action thus:

  def edit
    respond_to do |format|
      format.js { return render(:partial => 'edit', :layout => false) }
      format.html # edit.rhtml
    end
  end

Saturday, 26 July 2008

Best vs Good enough

I can't remember which I've heard - I think it was "good enough is the enemy of best", but it could as easily be the other way around. It's a bit confusing as I've definitely heard conflicting stories.

We are the best!

From one perspective, you want your new startup to be the best - because, in a lot of ways, that is what will distinguish you from your competitors. The difference in market-share between Google and Yahoo! is an obvious example. So is the difference between Amazon and... whoever comes second after Amazon.

Clearly it pays big to be the best in your niche. You need to be the best. "Good enough" not only won't cut it, but will relegate you as indistinguishable from the swamp of mediocrity in which all the other companies float.

In this sense, "good enough" is the enemy of "best".

Good enough. Ship it!

And yet, you need to *ship*, because if you don't have anything to ship, then the customers will never hear of you. This follows from the principle of "ship early, ship often". It's all too easy to get caught in analysis paralysis, endlessly perfecting what you've got without ever putting it "out there" for real users to get their hands on.

In that respect, "best" is the enemy of "good enough".

So which is it?

As with everything, you need a strategic balance.

You don't need to be best at *everything*. You'll wear yourself out trying. You just need to be best at what you've chosen for your market differentiator. Perhaps you're aiming to provide the best features for power users, or most comprehensible features for beginners, or even the nicest customer service. Whatever you decide on, use that focus to drive your quest for perfection. You can push for the best in this one area and let other areas be "good enough".

If you try to swallow the elephant whole, you'll never get it down. Instead, keep focussed on your core competencies, and don't worry *too* much about perfection in the other areas. You can always make them better in your 2.0 release.

Tuesday, 22 July 2008

More thoughts on Rails-doc 2.0

I'd been re-contemplating the Rails-doc 2.0 site and had begun to write a post about the minor annoyances I found, when suddenly up popped a new post by Jeremy on rubycorner.

I guess I'm not the only person whose experience was akin to: "OMG wow! *click* *click* hang on..."

My annoyances from rails-doc 2.0

There's not a lot wrong with rails-doc 2.0, the following are merely annoying.

Click depth

One of the benefits of the previous rails-doc site is that it's all there at your fingertips all the time. Rails has a deep hierarchy and now the only way into it is to search randomly. This can be fun - the auto-prompter is pretty good... as long as you know exactly what you're looking for. But on the old site, if you can't quite recall what you're looking for, you can browse down to the list alphabetically until you find it. With the new site you have to delete your old search and continue to try other names. Search helps, but having a browse-alternative is also helpful.

Another problem with deep click-hierarchies is that you can't get as much information at once. On the original site, all the methods are on the same page - and all you need do is click the anchor or scroll down to them to get a feel for the whole class. You don't have to keep clicking through and back if you want to look at each of the methods.

Searching code

...requires code-sensitive search. If I type "default_error_messages" it means something entirely different to a search with the words "default error messages" somewhere in the body.

version consistency

If I chose version 1.2.6 for the ActiveRecord class, chances are pretty strong that I'll want to keep looking at version 1.2.6 when I click through to the methods... or anywhere else, for that matter. I think there's a strong case for persisting the chosen version.

Is there really a problem?

As you might notice, these are just GUI annoyances not real issues, as such. So is there really a problem?

Jeremy points out that user-added notes are all well and good, but what we need is for the actual real rails-doc to be updated. That isn't going to happen here (at least with the current incarnation). His proposed solution sounds great - and I'll look forward to its release. Perhaps he should team up with the rails-doc.org folks ;)

But has it got anything right?

Jeremy actually stated this succinctly:

"If I’d known that all we needed was a good looking RDoc app, I could’ve fixed the problem a while ago."

Lets face it, we're geeks and we love shiny new toys... Rails being the epitomy of the shiny new thing. So yes, rails-doc 2.0 got one thing absolutely right - it provides a pretty interface that is easy and fun to explore. That's often all it takes folks like us to start contributing - so they got that right.

Monday, 21 July 2008

Rails doc 2.0

Finally some decent documentation for rails!

Like a number of rails enthusiats, I read Jeremy's blog post with a bit of trepidation. Mainly about the decline of rails blogging, it also points out the distinct lack of decent documentation for Rails... and firmly points a finger at all of us for not contributing to making it better. The worrying point is the conclusion that rails will decline in popularity due to a lack of available, comprehensive documentation.

Luckily, Rob Anderton has put my fears to rest. Rob's post has pointed me at the new Rails doc project. I've only just begun to investigate the site, but I can already tell it's far more useful than the standard Rails API I've been relying upon.

For one thing, it links the documentation to the version of rails you're using. This alone would have saved me a few hours of annoyance trying to get a method to accept parameters that are only available in a more current version than what's on our production server. :P

It also has a really neat keyword-search interface that prompts you with reasonable guesses, and a way for the community to add to the documentation (yay)!

All in all, it shows a lot of potential. So lets get cracking on building the doco up to a standard that developers in other languages have come to expect.

Sunday, 20 July 2008

One of Pradiptas children

Ok, so I put this into a comment on a previous post, but the phenomena has continued to spawn new children, so I thought I'd add to it.

Two days ago I opened my inbox to find it bursting with the results of an epic cascading thread. An overzealous recruiter had made a massive faux pas. He spammed 416 random rails-related folks with a Rails job offer... but put all their email addresses into the CC field.

The list ran the gamut of Rails, from those who'd barely touched it once, through the average rails developers (such as myself) right up to community luminaries such as DHH and Ryan Bates (of RailsCasts fame).

Reverse flash mob

After a few responses using reply-all, and the typical "don't use reply-all" (using reply-all), suddenly the mailing list became self-aware. Despite being accidentally gathered together in one place through an epic mishap, we recognised that we had a common interest... and a captive audience.

Most members of the group took advantage of this to chat about upcoming Rails conventions and what work we all did, and where.

Within two hours a google group (pradiptas-rolodex) was formed. Swiftly following were:

Sadly the wiki page keeps being deleted...

Anyway, it's been fun.

Highlights from the original thread

The post that began it all

I have a couple of Ruby on Rails position, wanted to know if you are
interested?


Max Archie
Technical Recruiter
Prodigus Source

The post that unified us as a community.

> PLEASE TAKE ME OFF THIS CRAZY EMAILING LIST!!!

Please KEEP ME ON THIS CRAZY LIST!!!

You all seem like pretty interesting folk, and perhaps serendipitously
we've all been added to the same ruby on rails marketing schloc. Even
more interesting to me, is that both my friend Harper and I are on it,
so hell they guy at least has good taste.

So in the spirit of fun email lists

Hi, I'm Anders Conbere, I Hack, run a co-working space in Seattle, and
love me some erlang and python.

What do you guys think of starting a google group I was thinking

"Igotfuckedinbanglalore"

~ Anders

P.S. I've thoughtfully removed our spammer from future communication

The best repsonse to the ad

Hi Max,

I am a recruiter who has an opening for a top-tier recruiter such as
yourself.  I need someone who can unwittingly set off the fury of *at least*
400 people, while ignoring all basic email etiquette.  Would you be
interested?   If not, do you know anyone else who is currently looking for
such an opportunity?

Sincerely,
Thanks for the mile long email thread out of freakin nowhere

Best representation of pradipta's mistake in code

PS:  Sorry for crashing anyone's iphone, but I couldn't resist replying.

module Recruiter
   class Email
      def initialize(site = "http://workingwithrails.com/")
          @recipients = Recruiter::EmailHarvester.harvest(site)
      end

      #
      # TODO: Really should change this to do a BCC instead of a TO
      # so 416 people don't get spammed and start an internet phenomenon of
      # replying to this message until the entire internet crashes.
      #
     def send_email(to_recipients, message_body =
File.read("really_bad_generic_email_body.txt") )
        @message = EmailMessage.new
        @message.to = to_recipients
        @message.body = message_body
        Mailer.send(@message)
     end

   end
end

Friday, 18 July 2008

Death by powerpoint

Check out this amazing powerpoint presentation* on making amazing powerpoint presentations!

* no, you don't often get to say that.

Thursday, 17 July 2008

Experienced Rails developer available - Sydney

Hi all, My contract is finally coming to a conclusion, so I find myself looking around for the next gig. I have a bit over three years full-time commercial experience with Rails, which I've been using commercially since just before it went 1.0. I'm looking for a contract in the Sydney area.

If you're interested, you can email me at home (24hr turnaround).

Tuesday, 15 July 2008

Little boxes, made of errors...

The standard styling for error boxes (at least with v1.2.6) looks ugly as per below.

Screenshot - old FieldWithErrors

The red doesn't go all the way around the actual box for form fields. Yuk! This is a result of the field in question going into a span that is styled - not the actual form field. Stylesheet code as below:

.fieldWithErrors {
  border: 2px solid red;
  background-color: red;
  padding: 2px;
  display: inline;
}

To make it look neat and pretty (as in the below image), use the following code instead:

.fieldWithErrors input, .fieldWithErrors textarea, .fieldWithErrors select  {
  border: 2px solid red;
  display: inline;
  padding: 2px;
}
Screenshot - new FieldWithErrors

Monday, 14 July 2008

Your customers *do* know what they want

I had a conversation with an old school friend last night; and we had a brief discussion about entrepreneurship as it relates to IT. He said he wanted to get out of the daily grind, and thought it'd be great to come up with a product and sell it. He was a bit leery, still, not sure about whether he could really "make it".

Having read a number of Paul Graham's fantastic articles I thought the idea was great, but I reminded him of how important it was to make something that your customers actually want. At which point he stated: "I speak to my customers all the time and in my experience they don't know what they want"

At the time I didn't answer. I could see he had a point, but it wasn't the one I was trying to make, and the conversation moved on. But now I think about it I think that he was simply wrong.

What question?

When you ask a user what content they want on their site or ask about styling or layout, you'll frequently bump up against a puzzled look and a lot of hemming and hawing as they try to figure it out. They don't have an answer ready for that and they probably didn't think about it before you asked. Often they have no idea what they want.

Many is the project where the requirements change like a fickle breeze as the scope creeps inexorably onward. It's obvious your customers don't know what they want. Right?

or do they? is it possible you're asking them the wrong questions?

What language?

Since I moved into freelancing, I've been studying up a whole lot more on business. One thing I was surprised to learn is that my customers are actually pretty savvy. They know their business, and they know exactly why they came to you to build their website.

Your customer knows they want a website that promotes their brand to get more customers, or to cross-fertilise their customer-base with their other product-lines, or to act as a tool that is a more efficient/less-expensive way of selling their products.

What they want has to do with their *business* goals, not with the specifics of the technology that will be used to achieve that goal. They know exactly what the goal is and whether they have reached it - and it has nothing to do with font-sizes or table layouts, or whether the "order" button should be on the product list-page or on the product's details-page.

If you don't know what your customer wants - it's most likely because you haven't asked them the right questions. But it's not enough to change what you're asking - you also have to change how you're asking it.

Your customer lives in a different world to you. You live in IT-world, but they live in the world of their business. Your customer uses a language that is different to IT language. They speak in the language of business - with market-penetration, conversion-rates and revenue. They don't operate in the realm of splash-pages, JavaScript or three-column-layouts. Business has its own language, its own way of organising and expressing ideas, and its own way of determining whether or not a set of actions will acheive a required result.

More than your job's worth

If you don't speak the language of business, then you'd better learn quick! These are your customers, and you already know they can't speak IT, and don't have the time or patience to learn. So it's up to you to do the translation service.

After all, that's your job. It's why you get paid the "big bucks" right?

It's your job to learn how to ask questions in such a way that your customer understands what you're asking and why. To translate their requirements into IT-jargon, and your questions back into business-speak. To express their choices in such a way that it gives them enough information to actually make the decision that you are asking of them.

Do they want flash or XHTML? does it matter to them if you use table or CSS-layouts?

No - what they want to know is "will this process of online ordering appeal more to my customers than the other, or will it be too difficult to learn?", "will my customers be able to find my products on this website, or will they get confused and I lose a customer?", "how can I use this software to give me better tracking of purchasing so I can promote or cross-promote appropriately?".

Tell them that if you place the "order now" button on the product-listing page, it could speed up the ordering process, at the expense of creating visual clutter. Explain that new users might get confused by this, but that it could aid frequent users to get through the process quicker. It then becomes a business decision: ie "are we aiming this site at new customers, or our returning/frequent customers?" - which is a much more meaningful basis for their decision-making.

So...

Translate your requirement questions into their language. Explain how their choice will impact them and their business goals. Explain the benefits in a way that they will understand. Make sure what you ask them leads back to their goals - not yours, and you'll find that they actually have a very good grasp of what they want.

Thursday, 10 July 2008

MySQL hates Timezones

We've started using the TzTime plugin which leverages the Tzinfo gem. It's pretty good. I've adjusted it slightly for my own preferences, but it works pretty well out of the box.

Unfortunately, we ran into some odd issues. Rails suddenly started to find records that it shouldn't. Eg if we were looking for all Event objects that occurred between two given dates, (and expecting to find none) suddenly we were finding one. Which played havoc with our tests :P

The occurred_at date on the Event object was clearly outside of the date-range we were looking at. We even tested to make sure the timezone wasn't stuffing it around and moving it into the date-window.

Eventually we discovered that the to_s(:db) command was printing the date out in a weird format using a T: '2008-07-10T00:50:23+00:00'. This is quite different from the standard MySQL format, which would have it as just: '2008-07-10 00:50:23'

MySQL needs to be set up properly to accept the "T-format" for DateTime objects and ours hasn't been. However, in our case it's much easier to simply extend Rails than to go through the required infrastructure politics to get global database settings changed (which will affect all the other projects in the business).

So I just updated the way that the to_s(:db) format works to always use the 'Y-M-D H:M:S' format as below.

ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(:db => '%Y-%m-%d %H:%M:%S')
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(:db => '%Y-%m-%d %H:%M:%S')

This seems to work just fine. It means that we'll need to update this if we ever change away from MySQL - but MySQL seems to be hard-coded into this organisation and seems pretty unlikely.

Note: when I first put this code into config/environment.rb it worked fine until I ran the tests - at which point I immediately got an error: uninitialized constant ActiveSupport. This went away the moment I moved the code below the Rails.initializer code (ie underneath the line that says: Include your application configuration below).

Monday, 7 July 2008

Not dead...

I'm not dead - just been In Thailand. I'll post again when I get up to speed.

Tuesday, 17 June 2008

assert email sent_to recipients

We have notifications sent out to users when their orders are finished processing. Recently we added "user groups" and so orders (which used to belong to a single user) now belong to groups of users. Which brought about the possibilty that a user could be removed from the group (and therefore could no longer see the order).

It's possible that this could happen between the time they submit their order and the time that the order has finished processing. We determined that, if the user no longer belongs to the group, they shouldn't get the notification - it should go to the next person in the group.

So our original notification code had something along the lines of:

  # Prepares an email that will notify the user that his calculation is complete
  def report_complete_notification(report)
    setup_common_values(report.user)
    @subject    += 'Your report is complete'
    body[:report] = report
  end

  # Details common to all emails from us
  def setup_common_values(user)
    recipients  user.email
    from        DEFAULT_FROM_EMAIL
    subject     "Our Funky Service - "
    sent_on     Time.now.utc
    body        :user => user
  end

So first we needed to update the notifier to call an appropriate 'recipient' if needed:

    setup_common_values(report.recipient)

Using the following recipient code in the model object:

  def recipient
    # always belongs to the original owner if no group is specified.
    return self.user unless self.group
    # use same recipient if creator still belongs to the request's group
    return self.user if self.group.users.include?(self.user)
    # otherwise grab the first person out of the group
    return self.group.users.first unless self.group.users.blank?
    # no users left - who do we notify?
    raise "HELP! The report: #{self.id} belongs to group: #{self.group.id}, but the group has no users so I don't know who should receive my notifications."
  end

As you can see, we aren't yet sure where to escalate notificaton if the group is completely empty. We have several options, but we're still waiting on the customer to get back to us and tell us what they want. The most likely option is to send an alternative notification to our admins.

How to test?

So now we need to test whether the appropriate person gets the notification. To begin with, testing whether an email went to a recipient, or set of them, isn't a pretty assertion, so I've abstracted it out into its own assertion (below). This also allows me to extend it if we need to check CC: and BCC: lists later. For now we don't use them, so "to" is good enough.

  # assert that the given recipients appeared in the recipient list of
  # the given email. If "want_match" is false - assert that *none* of the
  # given recipients were in the recipient list
  def assert_recipients(eml, recipients, want_match = true)
    recipients = [recips] unless recipients.respond_to?('[]') # arrayify if single item
    recipients.each do |recip|
      match = eml.to.any? {|r| r =~ /#{recip}/}
      match = !match unless want_match
      msg = want_match ? "email should have been sent to recipient #{recip}, but instead went to: #{eml.to}" : "email should not have been sent to recipient #{recip}"
      assert match, msg 
    end
  end
  # convenience function for asserting an email had recipients
  def assert_sent_to(eml, recips)
    assert_recipients eml, recips
  end
  # convenience function for asserting an email did not have recipients
  def assert_not_sent_to(eml, recips)
    assert_recipients eml, recips, false
  end

Now we can do various tests such as:

  # note - assume mailer.deliveries is instantiated as @emails in setup.
  def test_should_send_completion_notification
    order = orders(:completed)
    
    UserNotifier.deliver_completion_notification(order)
   
    assert_equal 1, @emails.size, "should have received one email to confirm completion"
    assert_sent_to @emails[0], order.user.email
  end
  def test_should_send_completion_notification_to_new_recipient_if_user_moved
    order = orders(:completed_for_moved_user)
    # sanity check
    assert_not_equal order.user, order.recipient, "user has moved group, but they are still showing up as the recipient"
    
    UserNotifier.deliver_completion_notification(order)
   
    assert_equal 1, @emails.size, "should have received one email to confirm completion"
    assert_sent_to @emails[0], order.recipient.email
    assert_not_sent_to @emails[0], order.user.email
  end

Monday, 9 June 2008

Rails Gotchas: Missing host error in unit tests

I finally got around to adding nice "and if you'd like to see your newly-created report, go to the URL: BLAH" links into my notification emails... a tiny nice-to-have that wasn't high on the priority list.

Suddenly my unit tests started spewing errors of the form:
ActionView::TemplateError: Missing host to link to! Please provide :host parameter or set default_url_options[:host]

The doco on default_url_options says you can add the "host" param on ActionController::Base, but no amount of monkey-patching in config/environment.rb seemed to do the trick.

Googling found a link talking about re-writing your URLs and it mentioned the problem is mainly seen in unit testing, and gave a couple of lines for a fix (as below)... but didn't say where to put them.

  include ActionController::UrlWriter
  default_url_options[:host] = 'localhost'

Experimentation showed that if you put them in your Notifer model (eg user_notifier.rb) they work just fine. Otherwise it just doesn't get into ActionMailer. :P

Tuesday, 27 May 2008

Inflector::controllerize

Ok, so I'm bored. After Ben's suggestion for improving POST redirections I decided to implement a "controllerize" inflector ;)

Not sure yet how to get this into String (so I can do: "user".controllerize). I tried going via class String, module Inflections, and via module CoreExt with no joy... so for now you still have to invoke it via Inflector. I welcome suggestions, but I really should stop distracting myself with nifty metaprogramming. ;)

Anyway, this is the code - stuff it into somewhere like environment.rb:

module Inflector
  # turns a given class name into the properly capitlkaised controller name.
  # This can be fed to "constantize" to get the actual controller by this
  # name
  def controllerize(word)
    # make sure it's the raw class name, camelize then add controller 
    "#{word.demodulize.pluralize.camelize}Controller"
  end
end

Examples of use follow:

>> Inflector.controllerize("user")
=> "UsersController"
>> Inflector.controllerize("my_widget")
=> "MyWidgetsController"
>> Inflector.controllerize("String::Thingy")
=> "ThingiesController"
>> ctrl = Inflector.controllerize("user").constantize
=> UsersController
>>

Link: 21 Ruby Tricks

Not an article, just a link to a good one. Peter Cooper has written a nice article named 21 Ruby Tricks You Should Be Using In Your Own Code. They're pretty neat! Most of them had me nodding along, but there were several that made my eyes widen in appreciation.

Wednesday, 21 May 2008

Rails gotchas: more TzTime

While I'm neck-deep in timezone land, I'll also address one little problem wih the tztime plugin. While running the functional tests I began getting errors like the following:

NoMethodError: undefined method `tzinfo' for #<TZInfo::LinkedTimezone: UTC>

I could fix it by forcing a timezone into the TzInfo object at the beginning of each failing test - but that seemed a bit of a hack. Looks like the problem has been submitted as a bug in rails ticket 11150. The suggested codefix seems to work just fine, but requires more hacking on your vendor/plugins/tztime directory. Still, it's a one-line fix, so not much to do.

The failing tests were replaced with a new error:

<Tue Apr 01 00:00:00 +0000 2008> expected but was
<2008-04-01 00:00:00 UTC>.

The failing line is in the functional test thus:

assert_equal Time.now.at_beginning_of_month, assigns(:month)

This is an instance of a problem I've seen before... ie that you don't seem to be able to compare Times with DateTimes, Dates or (now) TzTimes. The quick solution is to forcibly cast all your time-like objects into a common time-like object before testing for equality. My suggestion is to_datetime, which works for all the instances I've needed, thus the previous test case would become:

assert_equal Time.now.at_beginning_of_month.to_datetime, assigns(:month).to_datetime

Converting a TimeZone to a TzinfoTimezone

The TzInfo gem and plugins: tzinfo_timezone and tztime provide a bunch of timezone-related nifty things that intend to make your life easier by providing extra helper stuff on top of the standard Rails TimeZone class.

Personal preferences

However, I personally hate the dodgy-looking "Country/City" style of timezone list that the plugin provides. I much prefer "GMT+10:00 Sydney" provided with Ruby's TimeZone code. Especially when dealing with clients that can be expected to understand what a timezone is.

I figured I could take davantage of the niceties of TzTime/TzInfo for converting, displaying and updating activerecord etc... but use the standard Rails time_zone_select helpers on the user-preference form, and just convert the users chosen TimeZone string into a TzinfoTimezone before applying it.

But when I tried it, I suddenly started getting something along the lines of:

TZInfo::InvalidTimezoneIdentifier: no such file to load -- tzinfo/definitions/Tijuana

One big problem

Delving into the code, it seems that neither the TzInfo::Timezone class nor the tzinfo_timezone plugin's TzinfoTimezone class actually provide a method to convert from the standard Ruby TimeZone string and back... even though the plugin seems to do this internally (to generate the new list of timezones for the drop-down list).

This seems a bit of an oversight as the plugin already contains a mapping (conveniently named MAPPING) between the two.

The monkeypatch

The required code change is pretty small if done inside of tzinfo_timezone. The TzinfoTimezone class' new method tries to fetch out the appropriate timezone object, returning nil if not found. A one-line change fixes it so that after trying with the given timezone, it checks if it's in the given mapping before trying again thus:

  # replace:
  def new(name)
    self[name]
  end

  # with:
  def new(name)
    self[name] || self[MAPPING[name]]
  end

The form drop-down

Adding the field to a user is fairly simple - assuming the string column is named "time_zone" - I use the pretty standard:

<%= time_zone_select 'user', 'time_zone', TimeZone.all, {:include_blank => true} -%>

The Controller changes:

According to the tutorial on using TzTime, we put an around_filter in the controller to prep TzInfo with the logged-in user's chosen timezone (or a default if they have none, or there's nobody logged in). Becuase the plguin doesn't directly update the TzInfo::Timezone class, we have to alter that to directly call our updated method.

  # old way to set up default timezone
  around_filter :set_timezone
  def set_timezone
    # pull pref from the user - if they've supplied it
    if logged_in? && !current_user.time_zone.nil?
      TzTime.zone = TZInfo::Timezone.new(current_user.time_zone)
    else
      # otherwise use the environment's default = UTC
      TzTime.zone = TZInfo::Timezone.new(ENV['TZ'])
    end
    yield
    TzTime.reset!
  end

  # new way to set up default timezone
  around_filter :set_timezone
  def set_timezone
    # pull pref from the user - if they've supplied it
    if logged_in? && !current_user.time_zone.nil?
      TzTime.zone = TzinfoTimezone.new(current_user.time_zone)
    else
      # otherwise use the environment's default = UTC
      TzTime.zone = TZInfo::Timezone.new(ENV['TZ'])
    end
    yield
    TzTime.reset!
  end

A validation for good measure

just to add to the pile, a validation to make sure the user's chosen timezone actually exists...:

  validates_each :time_zone do |model, attr, val|
       model.errors.add(attr, "is not a valid timezone") unless val.blank? || TimeZone.all.any? {|tz| tz.name == val }
     end

Tuesday, 20 May 2008

Selling yourself tomorrow to buy today

Wow - I just read a really thought-provoking piece on The Register titled: Privacy? Forget it. Sell your brain and desires to the highest bidder

It discusses the future intentions of companies seeking to buy your purchasing habits; and provides insight into the reasons behind Phorm, Facebook's Beacon, and many of the cutting-edge market-data gathering machines out there.

Nothing new you say? So thought I until I got through it all - it's a fresh and entertaining angle on an argument that we may have heard a lot about - but which still has room for improvement. Well worth a read.

Here's a snippet to get you started:

All around us the toasters are getting smarter. Sadly, we don't seem to be keeping up with the program. We remain poor schlubs.
In the good old days, a toaster was just a toaster. It warmed bread and issued the odd electrocution. Tomorrow's toaster, however, brings with it a new set of functions that travel well beyond bread.
As we hear it, the toaster of the future will contain more silicon and communications systems. It will have sensors that can detect an upcoming failure, and alert you, via e-mail perhaps, to order a new toaster. Or maybe the toaster will order a new version of itself from the internet if you've enabled the self-upgrade feature. That future toaster may also talk to other gadgets in the home. Perhaps they'll agree on when they should shut down to save energy or to avoid a fire. Maybe they'll agree on the next brand for their upgrade cycle and make sure you get matching stainless steel appliances throughout the home or that your current penchant for pastel kit is obeyed...

Read on...

Friday, 9 May 2008

Lay a Wizard over your controllers

Adding a wizard interface to a pre-existing system

Our system requires a user to enter in a large data set and several preferences to complete their order. It all works ok, but new users can get confused when presented with a busy and convoluted screen full of scaffolds and forms - which the initial (advanced) interface currently looks like. So we decided to add a step-by-step "create your order" wizard to help new users get an idea of what they need to do in order to create, edit and submit their orders.

As it happens - the ordering (and number) of steps in the middle don't matter too much. It's all just entering random bits of information. As long as the user creates the order at the beginning and submits it at the end - it doesn't matter if they add their widgets first, or set up their preferences first. In fact, given we don't care about the specifics of the steps it makes some sense to make sure this wizard is highly flexible.

Lucky for us - Ruby gives us that luxury.

Now, we already know the system does all of the existing parts of the process - it's currently happily creating/updating and submitting orders as we speak. So "all we need" is a new overlay of views and a new path through the controllers.

But how to do that in a nicely independant way?

First - make thy controller

We'll need one of those, and we'll need an action for each step in the process - plus an index action thrown in for good measure. So lets start with:

script/generate  controller WizardController create_order add_widgets specify_preferences review_and_submit index

Adding an action per step lets us make nice, named routes that look good in the URL-bar for our user. They also make routing easy. We only need to add the route:

map.wizard '/orders/:id/wizard/:action', :controller => 'wizard', :id => nil

Note that this is great for all actions on an existing order thats saved in the db and has its own id. However, a new order won't have an id yet, so we'll need a route to cover that too:

map.new_wizard '/orders/new/wizard/', :controller => 'wizard', :action => 'index'

Here's the basic wizard controller. Note that we try to fetch out the order before each step with a common before_filter. The "index" action will dump the user into the second wizard-step if they specify an existing order id. This is so they can come back to the interface from, say, a list of orders and not get dropped (confusingly) on the "new order" page.

# controls the "new order wizard" functionality
class WizardController < ApplicationController
  # this list holds the current ordering of the wizard steps. It's used by the 
  # workflow-display code and anything that needs to know what the "next step" is
  WIZARD_STEPS = [:create_order, :add_widgets, :specify_preferences, :review_and_submit]

  before_filter :login_required
  before_filter :prep_for_step

  # the various steps in the process of the wizard
  # Note - they are named, rather than numbered so that they can be
  # adjusted/renamed/removed without worrying about their ordering.
  # There's no code as most of them all operate simply off the order 
  # (which is fetched out for every step) and have a same-name template
  def create_order
  end
  def add_widgets
  end
  def specify_preferences
  end
  def review_and_submit
  end
  def index
    # if we've found an existing order, go straight to the first data-entering step
    return render(:action => WIZARD_STEPS[1]) if @order
    render :action => WIZARD_STEPS.first # otherwise begin at the beginning
  end

  private ############################################################
  # preload the order, if an id has been passed  in - or create a temporary one if not.
  def prep_for_step
    return false unless logged_in? # to be sure, to be sure
    @order = current_user.orders.find(params[:id]) unless params[:id].blank?
    @order ||= Order.new(:user => current_user)
    @wizard_step = params[:wizard_step] # save current step to pass into templates
    true
  end
end

Make a template for each step

Each action will need an associated template showing the options available to a user for that step of the process. This template will probably import partials from your pre-existing views. After all, you're laying this interface over an existing one - so template re-use is a Good Thing. The view re-use gives similar operations a common appearance across both the wizard and advanced interfaces. This helps a user make the step from the simple to the complex interface, due to a pre-existing familiarity with the appearance.

So why not just use the same views as your existing system? There are a few benefits to having separate wizard templates.

First is to provide a unifying, common "wizard interface". Eg a visualisation showing where the user is in the process workflow. Possibly ven a subtle change in the page style to make sure a user knows when they are "in the wizard" as opposed to the rest of the site. Clues as to orientation are surprisingly helpful, especially to new users who are unfamilliar with the layout and content of your site. Every bit helps.

Second - this interface should contain extra instructions on the requirements of the current step. You know for a fact that the wizard will be used by the novice users of your system - so make sure you support their needs by providing extra help. The purpose of this wizard is to hand-hold your newbies through a difficult and complex procedure. Now that you've cut it up into bite-sized pieces, make sure you don't let them down by failing to explain each step.

Finally, the wizard interface should be much simpler than the normal view templates. As aleady stated, these users will be brand-new to your site. They don't want the advanced power-user features just yet - they just want to know how to get started. Any highly complex extras should be left out. You can keep the advanced uber-functions for when you user is confident enough to move beyond the wizard to your standard (now advanced) interface. Just keep the bare minimum that a user will need for this wizard to be functional, without crippling it so badly as to be useless. ;)

Displaying the workflow

Your user will need to know: a) what steps are in the wizard b) what step they are currently on. This is basically just a fancy tab-navigation structure, with the current one highlighted. I've covered this sort of thing in my tabbed navigation articles, so I won't repeat the details here.

You will need to store the set of steps somewhere accessible. In this case, we figured it made most sense to store the list as a constant in the WizardController itself. I also recommend using little images of right-facing arrows in between each "tab" to give the perception of flow.

I generally add a step-specific overview (eg "In this step, add widgets to your order") to get the user oriented. For us this goes directly underneath this tab-list so there's an obvious visual connection between the overview and the current-step. Don't make it long or it won't get read. Brevity gets the point across better!

Interfacing with the existing controllers

So, we have nifty templates that display what a user should be updating next - eg a list of possible widgets for the user to add to their order. So when they click on the "add widget" button - what happens next?

We already have an "add widget" action in our WidgetsController and we want to re-use that because duplicating controller functionality is just a nasty maintenance nightmare waiting to happen. So, the forms that are displayed on the templates need to point at the usual controller actions just like your original views. The problem is that when the WidgetController's "create" action is done, it's likely to send the user on to "order_widgets_path" (just like normal) - when we really want it to come back to the current step in the wizard.

So - how does the WidgetController know we want the wizard instead of the usual control path? and if so - how do we know what step of the wizard we need to pass on to? and finally, can we do all this without becoming terribly hard-coded and brittle?

Firstly, we can tell the other controller that we are coming from the wizard by passing the current wizard step in a variable in the required forms/links thus:

  <% button_opts = {:order_id => @order.id, :escape => false, 
                    'wizard_step' => 'add_widgets' } -%>
  <%= button_to('Add to order', new_order_widget_path(button_opts.merge(:id => widget.id))) -%>

This will probably require some hacking on any existing partials that have embedded forms to add a field-repeater eg:

  <%= hidden_field_tag('wizard_step', @wizard_step) if @wizard_step -%>

Now you can add a check in the relevant controller actions that tests if this step is present. If so, we need to go back to the given wizard step... but checking specifically for param[:wizard_step] is a bit nasty. What happens if we have to change how we specify that we're currently executing a wizard-step? So pull it into a common method that does a "test and redirect" thus:

In application.rb:

  # redirects the user on to the given step in the wizard process
  def redirect_to_wizard_step(the_id, step)
    return false unless current_wizard_step # allow controller to continue on as normal
    redirect_to wizard_path(:id => the_id, :action => current_wizard_step)
    return true
  end

  # Redisplays the current step in the wizard process.
  # Note: only use this if the current action was *not* successful.
  def redisplay_current_wizard_step(the_id)
    return false unless current_wizard_step # allow controller to continue on as normal
    # render template is necessary as we are likely not coming via the
    # wizard controller
    render :template => "wizard/#{current_wizard_step}"
    return true
  end

  def current_wizard_step
    params[:wizard_step]
  end

In WidgetController:

  def create
    @widget = Widget.new(params[:widget])
    @widget.order_id = params[:order_id]
    # to re-render wizard-step in view - if present
    @wizard_step = current_wizard_step 
    respond_to do |format|
      if @widget.save
        format.html { 
         # if we came here through the wizard - move on via the wizard process
          return if redirect_to_current_wizard_step(@widget.order.id)
          redirect_to order_path(@widget.order) }
        format.xml  { head :created, :location => order_widget_path(@widget.order, @widget) }
      else
        format.html { 
         return if redisplay_current_wizard_step(@widget.order.id)
          render :action => "new" }
        format.xml  { render :xml => @widget.errors.to_xml }
      end
    end
  end

That's about it - happy wizarding.

Tuesday, 6 May 2008

Rails gotchas: Data Migrations conflict with validations

So the problem is that we have some basic data that we like to preload into the database - eg an initial admin user. We created the data migration to do this and it ran fine, validated correctly and was loaded into the database for us.

Over time we added new migrations and new validations and at every step the db and code were in-synch.

Now we need to set up a new testing server. We ran the mmigrations from VERSION=0 and suddenly we get an error on the data migration along the lines of:

rake aborted!
undefined local variable or method `group_id' for #<User:0x3367bc8>

Obviously this column wasn't needed when we wrote that migration, and by now we already have the admin user in our system, so the migration hasn't been run on our latest code... until now.

The problem is that the code *now* requires a group_id for the user to be valid; but at this point we haven't reached the migration that adds the groups table to the database, along with the corresponding group_id column on the users table.

However, all the latest code is in our model, including the validation that requires valid user to have a group_id. *BUT* the group_id method simply doesn't exist yet, because ActiveRecord will only find that method by reflection once the table has the appropriate column.

We have a conundrum. We don't want to remove that validation, but we also need that data to be in the database.

So what can we do?

There are several nasty options we considered, such as adding a rescue-block around the validations and catching "just that error". Unfortunately this has the problem of hiding any future errors that we may need to fix. Allowing errors to get artificially caught, like this, can lead to them ending up in production code without us having noticed their existance. :P

We could also go back and delete the data-migration from the original file and generate a new migration for it. The new migration will be called after all the current ones and so will match match the current code. This just delays the inevitable, though. We will cimplu come up against the same problem at some future time when new validations are brought in. There needs to be some solution that will fix this for all time.

There were two reasonable solutions that we considered.

The first is to put the required data into a fixture. The fixture can be kept up-to-date with the code and therefore will always pass the validations of the present-set of code... it could then be loaded on-demand (via the console), or we could alter the original data migration to load the fixture instead of creating an ActiveRecord object. The latter has the same problems as the original issue - it will try to load in things like the group_id column at a point in the migrations that the column doesn't yet exist. The former would work - but we'd need to remember to do it every time - which means it's prone to user error.

The alternative involves a simple one-word fix... which is why it won out.

The Validations module overrides the standard ActiveRecord::save function with the save_with_validations function. This latter function takes a single, optional parameter that can tell ActiveRecord to ignore validations. ie you call save(false) and it will save the record without checking that the data is valid.

If we can assume that our own data in the data migration is valid for our purposes, then we can use this to solve the problem, as the validations will simply be ignored.

Thursday, 1 May 2008

Up or Out - the attitude for contractors

I've just read the article Up or Out on The Daily WTF. This is related to/based on Bruce Webster's article Wetware crisis: Dead Sea effect. They provide a fascinating look into the culture of the skilled IT workforce as it pertains to over-zealous attempts at retaining skilled workers.

So I got to thinking about how that applied to myself, and my past experience in IT. I've worked both as an employee and, recently, as a contractor - and I've certainly noticed the difference in attitude between the two.

I believe that contractors automatically have this "up or out" mentality. They know that their required skills are wide and varied, and that to keep ahead of the game, they need to keep their skill-level up. Staying in one job for a long term leads (eventually) to skill-stagnation. Not because the company no longer has anything to teach a contractor, but because of a case of diminishing returns.

As the article states, any skilled worker can learn a lot more by exposing themselves to new opportunities, than by sticking with a single firm. However, contractors seem to be far more aware of this fact than long-term employees (even the skilled ones). Contractors seem to have learned the lesson that current and diverse skills are marketable, and that in-house knowledge of a specific system, while beneficial to keeping a specific job, doesn't lead to expansion of a skill-set. This leads contractors to constantly push themselves to try new and different things to stay ahead of the game - to keep themselves a marketable commodity.

Employers don't seem to have kept up with this understanding. The majority of employers look poorly on hiring contractors (except for specific skills lacking in-house), compared with potential long-term employees. Contractors fall foul of the attitude mentioned in the article - the feeling that contractors are just "dating around" and aren't serious about their committment. This despite the fact that employers are no longer loyal towards employees (a job is no longer a career-for-life).

So are contracters flighty or are they simply being honest? We know that our skills improve by varied experience. Stagnating in a single shop isn't good for us *or* for the company - yet we constantly get the look-down-the-nose treatment as though we are far more unreliable than their indentured employees.

Workers and companies both benefit from fresh-blood. There is definitely a balance involved here - continual turnover can make it impossible to keep hold of institutional knowledge (which is why documenting procedures is so important!), but a company without fresh-blood will stagnate and lose market-share due to a lack of new and innovative insights coming into the company. Any firm can benefit from a good mix of the two. The fact that contractors have accepted and embraced this fact should be a sign of maturity, rather than flightiness.

I especially dislike that skeptical tone that comes with with the phrase "oh, so you're *not* interested in long-term employement". I feel I'm simply being realistic. But employers (and recruitment agencies) that employ the tone seem to imply I'm being disloyal... or committment-phobic. Neither of which is the case.

A few rare cases even seem to imply that I'm simply being greedy - shopping around for a better cash-deal, which is far from the case. While I'll take a higher salary over a lower one, I'm far more motivated by an interesting project and an opportunity to learn. That's why I'm in Rails rather than many other technologies that pay well. I would never take a higher salary just to do something that was mind-numbingly boring and, in my mind, dead-end. I'm just stoked that I get to work in a field that is cutting edge *and* well paid at the same time ;)

I have no problem with sticking around with a group that values me and that contributes to my own experience. My current contract (with SIRCA) has lasted more than a year. I fully believe that I have added value to this project. I also have learned a great deal. I have also had smaller contracts where I have provided some chunk of functionality or code-review - each of which added visible benefit to the site I worked upon, and also provided an opportunity for me to learn some new aspect of the technology I work with. This is an equal-footing relationship - where both parties receive value.

My previous employment has included roles in which I felt my input was not valued, and in which I had no mentor from which to learn. Effectively I was gaining no value from them, and they were not gaining full value from me either. I stuck around for a long time trying to help out with the project - and eventually left due to burn-out. In hindsight I should have left far earlier and moved on somewhere that I had an opportunity to grow, and that valued the insights I was able to bring. It would have been a better opportunity for me - and the company I was with could have employed somebody that they would have felt comfortable with - increasing their return-on-investment as well.

In a free market - there's no point in sticking with a relationship that is not valuable, or which provides value only to one party or the other. I won't leave for reasons of greed or lack of committment - I'll only leave if you and I are no longer gaining enough value for the professional relationship to be worthwhile.

If an employer doesn't feel that this is reasonable - then we need a good talk on the concept of "fairness".

Healthy economic relationships come about when both parties benefit. Anything else doesn't make economic sense. When the benefit to one or the other fades over time, then it should be understood that this will lead to the eventual ending of the relationship. It's simply a matter of good business practice - and shouldn't be looked on as "lack of committment" any more than moving to a cheaper/more efficient supplier.

</whinge> :)

Update: Bruce Webster has written a follow-on article: some thoughts on up or out which gives a few ideas on how to avoid developer-churn or the Dead-sea effect (and even counteract the thermocline of truth) by providing a non-managerial track for techies to follow. I completely agree. If there was a way "up" that didn't involve "out" I'd go for it!

Monday, 28 April 2008

redirect_to POST

Our current site uses the acts_as_authenticated plugin. When a user's session has timed out, we redirect them to the login page, and save the URL they were trying to reach. If they successfully login, we do a redirect to that URL. This all comes as standard in the plugin.

The problem is that redirect_to doesn't seem to preserve the HTTP method (ie GET or POST etc). Most of the time, a user will have clicked on a link, or have a link saved in their bookmarks. The URL will be something like /users/42/orders which would be a a GET-based request that is supposed to show the users current orders. However, sometimes a user will have left their browser session open while they went off to lunch, displaying their set of orders... along with a set of buttons that, say, delete or clone an order.

The problem comes in when this timed-out user tries to click on one of those buttons. A timed-out user that clicks on, say, the "Clone this order" button gets the following error: no route found to match "/user/42/orders/23/clone" with {:method=>:get}

Buttons (which are really a form-post) generally have an HTTP POST action - even the ones with Rails-faked-up PUT or DELETE actions are really POST under the covers. Our nicely RESTful application is set up to not accept GET-based requests for dangerous (non-idempotent) actions such as "clone".

Now, acts_as_authenticated uses two methods for storing/redirecting to URLs. The first is called store_location. The code here grabs the URI that the user requested, and stuffs it into the session. The second is called redirect_back_or_default - it tries to redirect back to the uri stored in the session - or to a given default.

The method: redirect_to is the Rails-standard way of sending you on to another URL via an HTTP redirect. Unfortunately, it seems that this method only tries to use GET-based URIs, so when we tried redirecting to the POST-only URI for the "clone" request, we got the routing error.

At first I thought all that was needed was to save the request's HTTP method, and pass that into redirect_to. Saving the method is easily done, and I stuffed it into the session along with the URI. Then I dug around in vain trying to find out how to pass in the HTTP method as a parameter.

Reading all the doco yielded no joy, nor did digging into the rails core code to see how it deals with uris, sessions, requests or redirection. It wasn't until I delved deep into the HTTP spec itself, and asked a few questions on RoR Oceania that I finally confirmed that it simply isn't "done" to try to redirect a user to anything other than a GET-based URI.

This is somewhat disappointing, as I don't see why a GET has to be treated like something special by HTTP - the use-case for allowing redirection to any other verb is there. :(

Still, there's no getting around this limitation, and the system must fail gracefully (which the routing spew doesn't). So the next best thing is to capture an attempt to access a non-GET request and try something different.

I figured that most of the time when a user tries to get to a non-GET request, it'll be from a button from another page on the site. eg, they'll have left their brower open on the "My orders" page, and have clicked a button from there. It's far less likely they'll somehow have hooked up a bookmark to a POST-based URI, and just as unlikely that some other site will have a button to our site. Therefore, I figued the best option is to try sending the person back to the page they started on (ie the "My orders" page), rather than the page they are asking for (ie whatever button they clicked on).

This can be solved fairly easily. If the user asked for a GET-based request we have no problem, just store it as usual. But if they asked for something else, then we need to save the referer-URI instead. I've also added a flash error to tell the user what happened, and why they have to click the button again.

So the usual acts_as_authenticated code becomes:

    # Store the URI of the current request in the session.
    #
    # We can return to this location by calling #redirect_back_or_default.
    def store_location
      # if the user has asked for a non-get request (eg posted a form). We
      # can't redirect to that - so try getting the referrer (probably the
      # index page they came from). We will post them back to their previous
      # page and warn them about what is going on
      session[:return_get] = request.get?
      session[:return_to] = request.get? ? request.request_uri : request.env["HTTP_REFERER"]
    end
    
    # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      # if the user had asked for a non-get request (eg posted a form). We
      # can't redirect to that - so we must warn the user that they will not
      # be going where they were expecting
      flash[:error] = "You clicked on a button or submitted a form, but we cannot redirect you back to that. Please try submitting again." unless session[:return_get]
      redirect_to(session[:return_to] || default)
      session[:return_to] = session[:return_get] = nil
    end