
One of the few times Rails let me down while developing ZingLists was how it deals with class inheritance in ActiveRecord. Actually, unless you want Single-Table Inheritance semantics, you may as well pretend it doesn’t exist, because I couldn’t find a clean way to do it.
Here was my particular problem:
Out of the box, Rails likes to construct URLs that usually end with a numeric ID when you’re dealing with resources. There is a method, to_param, that the URL helpers call when they want to turn an object into an ID suitable for a URL.
Conventional wisdom says that Google likes URLs that are meaningful. A string of numbers at the end doesn’t mean anything, which is why you often see URLs ending with a snippet from the page title. Blogs often do this (just look at the top of this page, if you’re not reading from the index).
So, to get nice URLs, modify to_param. But what if you only want to do it some of the time? Inheritance usually solves this problem: customize in the derived class. But ActiveRecord forces STI on you if you do this, and maybe you don’t want that.
In ZingLists, I have a single table, lists, that contains all of the lists in the system, whether they are private lists for a member or lists that a member has published to the community. It pretty much has to be this way, since publishing a list does not fix it in time. The member may (and probably will) continue to use it for themselves, and if they add to it, I’d like those changes to be available in the public view immediately, with no extra work.
I want public list URLs to be nice for Google, but don’t have any desire to junk up private list URLs, too. How to solve this problem?
Duck Typing. The URL helpers don’t care what kind of class you hand them, as long as it responds to to_param:
class PublicList def initialize(list) @list = list end def to_param "#{@list.id}-#{@list.name[0..29].tr_s(" ", "-").gsub(/[^-a-z0-9]+/i, "")}" end end
Now, when I want to create a pretty URL for Google’s benefit, I just have to instantiate a wrapper around the real object:
public_list_path(PublicList.new(list))
(Wishful thinking: What would be really neat is a facility where you can get STI-like semantics, but provide your own definition of how to differentiate between the types of object, rather than have it hard-coded to a column called “type” that contains a string representation of the class to instantiate.)
Last week, we finally launched our first product: ZingLists. It’s a community-oriented site for making and sharing lists. What kind of lists?
ZingLists is not the first time I’ve built a real web site, but it is my first serious project that uses Ruby on Rails and I have to say that it has been a very enjoyable experience. There is only one thing I can think of where I wanted to do a little more than Rails offered out of the box, and there wasn’t already a hook somewhere. (More about that later.)
The site is quite young at the moment and so public content is a little thin, but it will get better over time. Feature-wise, the site is very useful. I have had my personal to-do lists running from it for a couple of months, and have been using it for more generic list keeping as well.
There is almost always room for improvement, though, so if anyone has any feedback, feel free to leave a comment here or drop us a note at the support page.
The application I’m working on at the moment deals with a lot of dates and times, and its userbase could span many different time zones. Dealing with time zones and converting from one zone to another is tedious work, but there are some things you can do to make your life simpler.
First, always think in UTC. If not for daylight saving time, you could probably ignore this rule, but thanks to DST, you can’t. DST takes a relatively simple add or subtract and turns it into a tangle of what-ifs. Some zones don’t observe any daylight saving time rules. The zones that do may change when DST starts and stops from year to year.
Thinking in UTC means storing your times in UTC in the database, without exception. Rails gives you only a little help in this area, with ActiveRecord::Base.default_timezone. It only helps you with created_at/on and updated_at/on. It won’t touch your other timestamp fields.
Bugs often come about by missing little details, and forgetting to get a Time instance in UTC instead of localtime is just the sort of thing I know I’d do eventually. Ruby makes fixing this once easy. Reopen the Time class, and change the behavior of Time.now. Put this in your environment.rb:
class Time def self.now_utc return now_local.utc end class << self alias_method :now_local, :now alias_method :now, :now_utc end end
I think this gets you 90% of the way home. The remaining 10% is handling display issues, and Jamis Buck’s TzTime helps immensely with this part. Set TzTime’s zone at the start of each request to the zone of the user making the request, then make life even easier by ensuring you always use a helper for displaying dates and times. Mine looks like this:
def datetime(object, options = {}) return "argument is a #{object.class}, not a Date or Time" unless object.is_a?(Date) || object.is_a?(Time) format = if object.is_a?(Date) || options[:date_only] then "%b %d, %Y" elsif options[:time_only] then "%I:%M %p" else "%b %d, %Y %I:%M %p" end object = TzTime.zone.utc_to_local(object) if object.is_a?(Time) && object.utc? return object.strftime(format) end
To date, I’ve only found one gotcha, and while it was aggravating to find, it was easy to fix. The TMail that ships in Rails 1.2 (as part of ActionMailer) has a nasty habit of ignoring the @sent_on instance variable when sending mail via SMTP. It will always set this header, contrary to what ActionMailer’s RDoc tells you. Unfortunately, it sets the header using Time.now, which returns UTC with the above modification, but marks the time in the local zone. End result: if your localtime is behind UTC, the mail looks like it’s sent in the future. To fix, put this in environment.rb:
class TMail::Mail def add_date end end
With this change, TMail will never add a Date: header, allowing the MTA to add it itself.
Much to my surprise, one of the hardest things I’ve had to do while starting a new company is come up with a name for my product. Naming things isn’t one of my strengths, but the domain squatters have made this task much, much harder.
The biggest problem is that you can sit down and come up with a list of dozens of potential names, and then find that only a handful aren’t already registered by someone. The ones left are generally not very good. 37Signals says not to worry about getting the perfect domain, but I’m not yet convinced that you can ignore word-of-mouth (people literally speaking your domain name to their friends).
So if most “good” names are taken, that means buying one from a squatter. This brings me to my main point: negotiating with a domain squatter for a name is a lot like dealing with a car salesman. Usually, when you buy a car, the first offer or two from the salesman is outrageously high. He just hopes you haven’t done your research and that you’ll bite. Even armed with research, sometimes they won’t deal. The best thing to do is to get up and walk away. If they really want to sell a car, they’ll blink.
This is what happened to me. I made the lowest possible offer to start negotiations for a name I mostly liked, but wasn’t in love with. The reply was $800. Within two weeks, and mostly because I stopped responding to his offers, he came down to $150. I walked away, and he blinked.
The problem for squatters is that most domain names aren’t really worth anything at all on the open market. They’re only worth something to someone who has an idea or a partially finished application and needs a name. If the price is outrageous enough, we’ll just come up with something better. That gives us tremendous leverage: when the market for buyers is just a handful of people in the world, it’s essentially impossible to get them into a bidding war. So really what we’re trying to do is figure out the absolute lowest price the squatter will sell at and not hold on to that domain out of spite.
Unfortunately, even at $150, that means he can hold 14 or so other names that never sell and still break even, so this problem isn’t likely to go away any time soon.
I’ve immersed myself in Ruby and Rails for the last several weeks, working on a project I hope to complete in the next month or so. The other day, I created a new mailer to send a welcome message when a user registers with the site. I’d done a mailer before and written a unit test for it, and was surprised to see the following when I wrote a unit test for the new one:
1) Error:
test_welcome(UserAccountMailerTest):
ActionView::TemplateError: confirm_user_url failed to generate from {:controller=>"users", :email_address_id=>"1", :secret=>nil, :action=>"confirm"}, expected: {:controller=>"users", :action=>"confirm"}, diff: {:email_address_id=>"1", :secret=>nil}
It looks like ActionMailer isn’t happy when a mail template makes use of named routes when run inside the test framework, but that doesn’t make any sense. Here is the route, from routes.rb:
map.confirm_user "users/confirm/:email_address_id/:secret",
:controller => "users",
:action => "confirm",
:requirements => { :email_address_id => /\d+/,
:secret => /\d+/ }
The error seems to say that the routing framework is not parsing my route correctly, and fails to see that :email_address_id and :secret are both requirements for the route (”expected:” specifically excludes those two parameters).
This is one of those times where the error message didn’t help as much as I hope it would. It turns out that the entire problem is because “secret” is a new column, and I hadn’t added it to my fixtures yet. Fixing that fixes the problem, and my test passes.
Usually, the net is amazingly wise and I can find either an answer or a hint in the right direction after a few searches. This time, I had to rely on a “hey, wait a second…” moment.
A couple of months ago, there was a little bit of discussion on the web about customer service. Ryan Carson (of Carson Systems) related his experience from the business owner’s perspective that didn’t go very well.
Surprisingly, it doesn’t take an abusive customer to get the brush off from a company. My wife’s cell phone got wet one day. Not soaking wet, but wet enough that within a week or so it started turning itself off at random times.
She went shopping on eBay, found the same model (used) for $30 and bought it. It is locked to the Cingular network. We’re Cingular customers, though the merger with AT&T Wireless, but the phone doesn’t know about the merger. We hadn’t considered this, and her old SIM card didn’t work in the new phone.
Calling Cingular doesn’t help. They’re willing to sell us a new, Cingular SIM card for $30 or sign us up to a new contract and give her a new, free phone.
A quick Google search later and we’re reading instructions for how to unlock the phone ourselves, for free. Now her SIM card works fine in the phone. She’s moved over her contacts, wallpaper and wants her old ringtone back, too.
Next problem: the used phone from eBay isn’t set up quite the same and is having trouble getting to the ringtone she’s purchased. Another call to tech support is equally as fruitless. They won’t help us at all and outright lie about the phone not having the same features since it’s a Cingular phone and we’re AT&T customers. Or maybe it’s because it’s an unlocked phone. It’s so bad, they can’t keep their lies straight. She got it working herself by comparing the settings of the two phones side-by-side.
The moral of the story is this: we’re long-time customers, past the end of our contract, and nothing but momentum is keeping us with Cingular. They should realize this, and make an effort to keep us happy. Instead, they attempt to squeeze more money out of us and lie. All they have accomplished is to give us another reason to leave.
On a different note, how much fun is Ohio weather? Last week, it was 80. Today, 28 and:

I arrived in Silicon Valley in mid-1997, in the middle of the dot-com IPO boom. Amazon.com started trading on the public markets in May of that year, well before they would turn a profit, and saw a 30% jump in their stock price that day, even after pricing it 28% above the high end of the price range.
My first company was pure Internet bubble speculation. They’d been around for years already, taken several rounds of venture capital and had wildly changed their strategy from hand-held communications software for Apple’s Newton to more general client/server software trumpeting the then-favorable “push” buzzword. As it happened, my girlfriend at the time worked for Pointcast, one of my company’s favorite targets of ridicule in the press. Of course, neither company ended up doing anything in the long run.
I stayed in the Valley for nine years. I think my experience there is probably fairly typical. One employer blew up spectacularly. A few ran out of money. A couple were acquired. None were news-worthy successes. I do know one guy who did really well and several others who did just OK.
I’m fairly confident that, had I stayed, eventually the stars would align and I’d ride one of the exit strategy trains to (modest) stock riches. Maybe I wouldn’t retire, but it’d buy a nice house and I could stop thinking about if sending my kids to the public schools was the best thing to do. Now, though, my priorities have changed. I don’t want to keep playing the start-up engineer game. I have ideas of my own and things I want to try, but lack the money connections to fund them. With a family to support, mac & cheese and ramen isn’t a realistic option.
So we left Silicon Valley and came back to Ohio. The cost of living here is far lower, we have extended family nearby (so my kids will know their grandparents) and I’ve managed thus far to retain some consulting work back in the Valley.
That can’t last forever, though, which brings me, finally, to the point of this blog. I’m going to strike out on my own and work on some product ideas, supplementing our income with consulting. I’ll admit it: this blog is probably more marketing than anything else. It can’t hurt to get my name out, let people get a feel for who I am and what I can do. If even one client finds me from this, it will have served its purpose. If new or potential clients read this, I hope it allows them to be more comfortable with an unknown engineer, until I’ve had the opportunity to prove myself through my work.
My plan is to keep topics here fairly technical. Some of them will likely revolve around my projects, others may comment upon experiences and lessons learned with clients. If I have something worthwhile to add to something I read elsewhere, that’s fair game, too.
I am eager to see where this leads.
Contact us: info (at) lightyearsoftware.com | Press
Copyright © 2007-2009 Light Year Software, LLC. All rights reserved.