February 28th, 2008
Musing on Google Reader's "Share With Contacts"
So now you can share Google Reader items with GTalk friends and it’s really seamless and easy to do. Of course, the obligatory backlash happened and whatnot. Ok. But ultimately, this new feature of Google Reader has some interesting ramifications.
When your friends share something with you, you can then click the share button again, which will share it with your other friends, who can then…you get the idea. What does this remind you of? That’s right, the Share button is Web 2.0’s response to the joke email forwarder! While I say that in jest, it really does provide some enormous viral potential for content (assuming you have friends who like the same things you do, and so forth). I don’t think it’s had much of an impact yet, but I think that as usage of this feature spreads, there will be a general gain in the collective knowledge of a number of groups. Everyone follows different blogs but the good stuff from all of everyone’s blogs is now available to everyone.
The other thing that is interesting to me is a question of ethics: do I share my own blog posts or not? This feels very much like the “should I vote for myself” idea, and it’s hard to decide where to come down. On the one hand, of course I think everything I write about is very interesting. I wrote about it, after all. On the other hand, it feels like such self-promotion that I’ve yet to be able to pull the trigger. Maybe if I did my Feedburner stats would improve somewhat. Probably not.
February 25th, 2008
Codebite: Generic "New Today" for Rails Records
Often times in an application you might want to know how many of a given model were created today. Rather than writing a custom method for each model, let’s keep it DRY and extend ActiveRecord:
class ActiveRecord::Base
def self.new_today
if self.new.respond_to?(:created_at)
self.count(:all, :conditions => "created_at > (NOW() - 60*60*24)")
else
nil
end
end
end
This will return the number of records created in the last 24 hours by calling the model with new_today (e.x. User.new_today). It will return nil if the model does not respond to the created_at Rails timestamp attribute.
February 22nd, 2008
Disparate Sources: Car Keys and Regret Minimization
This article kicks of the first of a series I’m christening “Disparate Sources,” where I will take something totally unrelated to computing and web applications and turn it into a metaphor for something important to remember when building an application. This time: car keys and exit status.
My car, and I believe most vehicles, have the following two behaviors in regard to keys in the ignition:
- They will not start the car if the car is not in park
- They cannot be pulled out of the ignition if the car is not in park
At first blush these ‘features’ might seem to be more aggravation than benefit: the system doesn’t behave how I expect it to when I try to turn the keys and can’t. In fact, it might be downright confusing if I believe everything is in order and can’t start the car or remove the keys. However, if you think about it, the auto manufacturers are partaking in a design principle I call regret minimization; that is, interfaces should be designed such that each action the user may take will be the one that leads to minimal aggregate regret.
What does that mean exactly? Well, in the case of the ignition it’s quite simple: these features are in place to prevent you from inadvertently crashing your car into someone else’s. If you start the engine while the car’s in drive or reverse, the idling force of the motor might well propel you into something you’d prefer avoid without your noticing or being able to stop in time. In the key removal case, it’s preventing you from removing the keys so that you don’t leave your car on a hill in something other than park: you might come back and find your car in a very different place and much worse condition than you left it.
By introducing an inhibiting action that demands the driver’s attention, the car makers are better able to minimize the extremely regretful situation of having a car accident while introducing what is, in the vast majority of cases, only a minor annoyance. They are minimizing the regret of the user experience.
This has several analogs in user interface design. The ultimate representation of regret minimization is Undo: you can literally cancel out a regrettable action, setting the state back to a more agreeable time. The simple fact that anywhere it is feasible to do so, actions should have the ability to have zero consequences. In web applications this can be difficult, but there is still a comparison that can be drawn.
When you are using a web application and click “Back” or type a new URL, sometimes you get an unexpected action: rather than going back or going to the new URL, the browser pops up an alert, usually with some copy along the lines of “Are you sure you want to navigate away from this page?” There’s little question that this alert is going to annoy you, at least to some extent. By nature we don’t like things that stop us from doing what we were trying to do. However, in the perspective of regret minimization, annoyance is by far the lesser of two evils. The alternative is losing any unsaved data, which might be anything from a couple lines of contact info to a blog post someone has pored over for two hours without saving.
Navigation blockers such as this are not the ideal solution to the problem: Undo and Autosave perform much more robust and less annoying implementations of regret minimization. However, if the choice is between no protection against unintended actions and annoying protection, it is the better strategy in the long run to implement the annoying but safe solution. Every effort should be taken, however, to ensure that it is only when unsaved data is present on the page that one of these blockers reveals itself.
So the next time you try to click away from a Google Document or pull the keys out of the car before you shift into park, try to stifle the annoyance and think what might have happened if the system hadn’t stopped you. You might regret it.
February 20th, 2008
Integrating raw post images with attachment_fu
I’m not sure how common of use case this is, but I recently had to work with an image being raw posted to my app via a Flash application and had a need to push it through attachment_fu for thumbnailing and to integrate with my models. After reading up on request.raw_post in the Rails API, I came across a blog post that gave me most of the solution I needed. Basically, we need to spoof all of the attributes that attachment_fu looks for when it parses the uploaded data: the content type, the original filename, and the data itself. My modified version of LocalFile looks like this:
require 'tempfile'
class RawFile
attr_reader :original_filename
attr_reader :content_type
def initialize(filename, content, content_type)
@content_type = content_type
@original_filename = filename
@tempfile = Tempfile.new(@original_filename)
@tempfile.write(content)
end
def path #:nodoc:
@tempfile.path
end
alias local_path path
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.send(method_name, *args, &block)
end
end
Which borrows heavily from the aforementioned blog post with the exception of getting the content passed directly into the initializer instead of being read from a file and getting the content type directly. Now I was ready to write my controller action to take the raw post data (in this case, raw JPEG data), create a Tempfile using it and subsequently pass that through to attachment_fu as though it were an uploaded form item. Here’s the relevant part of the action:
def create
image_params = {:user_id => params[:uid], :uploaded_data => RawFile.new(params[:name],request.raw_post,"image/jpeg")}
@photo = Photo.new(image_params)
end
This simple little bit of code will take a filename parameter passed through along with a raw post of JPEG data and create a new RawFile from it, which can be passed as the uploaded_data attribute of the attachment model. So if you need to integrate attachment_fu or any other Rails upload solution with raw data, this might be a way to do it!
February 19th, 2008
ActiveRecord::Base.create_or_update on Steroids
I’ve been working a lot with seed data in my applications lately, and it’s obviously a problem that can be pretty aggravating to deal with. I ultimately liked the db-populate approach the best with the addition of the ActiveRecord::Base.create_or_update method. My one problem with it is that it’s based entirely on fixed ids for the fixtures, which is a pain to deal with when loading up your attributes from arrays. Here’s an example of what I was doing:
i = 1
{ "admin" => ["Administrator", 1000],
"member" => ["Member", 1],
"moderator" => ["Moderator", 100],
"disabled" => ["Disabled User", -1] }.each_pair do |key, val|
Role.create_or_update(:id => i, :key => key, :name => val[0], :value => val[1])
i += 1
end
I really don’t like having to put the i in there to increment up. Not only is it messier code, but it would be dangerous if I wanted to move around the roles. I also don’t want to have to have an id explicitly in each entry since I really don’t care what the id is. So I thought I would hack up a better solution for this create_or_update scenario and this is what I came up with:
class << ActiveRecord::Base
def create_or_update(options = {})
self.create_or_update_by(:id, options)
end
def create_or_update_by(field, options = {})
find_value = options.delete(field)
record = find(:first, :conditions => {field => find_value}) || self.new
record.send field.to_s + "=", find_value
record.attributes = options
record.save!
record
end
def method_missing_with_create_or_update(method_name, *args)
if match = method_name.to_s.match(/create_or_update_by_([a-z0-9_]+)/)
field = match[1].to_sym
create_or_update_by(field,*args)
else
method_missing_without_create_or_update(method_name, *args)
end
end
alias_method_chain :method_missing, :create_or_update
end
Basically, this allows me to call create_or_update with an arbitrary attribute as my “finder” by calling Model.create_or_update_by(:field, ...). To give it a little taste of syntactic sugar, I threw in a method missing to allow you to name a field in the method call itself. So now the code I wrote before can become this:
{ "admin" => ["Administrator", 1000],
"member" => ["Member", 1],
"moderator" => ["Moderator", 100],
"disabled" => ["Disabled User", -1] }.each_pair do |key, val|
Role.create_or_update_by_key(:key => key, :name => val[0], :value => val[1])
end
This is much cleaner and prettier to look at, and also makes sure that it is keying off of the value that I really care about. This new create_or_update combined with db-populate creates the most powerful seed data solution I’ve yet come across.
February 14th, 2008
Regular Expressions: Not Just for E-Mail Validation
Regular expressions are a very powerful, but very confusing tool. It takes a long time to figure out the grammar, and even today just looking at a regular expression makes my brain go numb for a moment (but writing them is much easier). However, they’re good for more than just validating e-mail addresses. They can be a powerful everyday tool if you happen to need to reformat some text.
For example, I recently needed some seed data for an application and wanted to get 50 girl and 50 boy names. I did a quick google to find popular names and came to the Top 100 baby names of 2006. Great! I copy and pasted the names into TextMate and I was left with something that looked like this:
1 Emma
2 Madison
3 Ava
4 Emily
5 Isabella
6 Kaitlyn
7 Sophia
8 Olivia
9 Abigail
...
Now, that’s pretty simple, but I wanted it in a form like this (Ruby array shorthand):
%w(Emma Madison Ava Emily Isabella Kaitlyn Sophia Olivia Abigail...)
Rather than either retype or manually delete the numbers and proceeding tabs, I just wrote the following find and replace in TextMate (make sure you check the Regular Expression option):
Find: ^[0-9]+\t([A-Za-z]+)\n
Replace: $1
That is, find anything that has a line starting with a series of 0-9 followed by a tab followed by some letters followed by a line break, and replace it with just the letters and a space. This leaves us with the following:
Emma Madison Ava Emily Isabella Kaitlyn Sophia Olivia Abigail...
Which is almost exactly what I wanted to begin with. Now I’ve saved myself a little bit of time and a lot of repetitive work. You’d be surprised how often using regular expressions to reformat some copy and pasted text comes in handy.