This Year's Entry
Valerie's Mom's coming for a visit next week, and so, in keeping with tradition, it's time for a little Perquackey smack talk:
Yonder bridge doth arch and spanOver river, treacherous land,Up from rocks on which are dashedRemains of jumpers,Life's check cashed.
Oh, their lives they cut so short,Seeking damages, retorts,Ere they learned that winner's brayRings naught but hollow; their decayHaphazards forth inOd'rous ruin,Odious runes ghast dark and grey.
Decrypt ye there the message lying,Kindred spirits' fate decrying,Ne'er again sweet light of dayOversundring their dismay.Withhold your taunts, you French ka-nigget,Step down from tower high and bri'g it.Nine, nay, ten dice all that matterOn this table, curs'd idle patter.Blessed be the man whose letters speakOut for themselves, and if they're weak,Unbridled fury does not seek.
Nine days until the tourney fair, tenDice plus three of color rareSuffice. And may the sweet light glare.
Default Association Attributes in ActiveRecord
I cringe a little bit when I see a controller like this:
class Admin::Subscribers::InvitationsController < Admin::ApplicationController
before_filter :load_subscriber
before_filter :build_invitation, :only => [:new, :create]
def build_invitation
@invitation = @subscriber.invitations.build(params[:invitation])
@invitation.email = @subscriber.email # *shudder*
end
end
While it’s almost okay, it irks me to have to explicitly set the Invitations’s email address in the controller12.
An Insight
What I really want is to set the email address when I make an Invitation for a Subscriber. There should only be two ways to say that:
@subscriber.invitations.build
@subscriber.invitations.create
A Solution
So, let’s place the responsibility for setting the Invitation’s email address directly inside the Subscriber’s has_many association:
class Subscriber < ActiveRecord::Base
has_many :invitations, :as => :source, :extend => DefaultAssociationAttributes do
def default_attributes
{ :email => proxy_owner.email }
end
end
end
module DefaultAssociationAttributes
def build_record(attrs, &block)
super(with_default_attributes(attrs), &block)
end
def create_record(attrs, &block)
super(with_default_attributes(attrs), &block)
end
def with_default_attributes(attrs)
default_attributes.merge((attrs || {}).symbolize_keys)
end
end
And then clean up our controller:
class Admin::Subscribers::InvitationsController < Admin::ApplicationController
before_filter :load_subscriber
before_filter :build_invitation, :only => [:new, :create]
def build_invitation
@invitation = @subscriber.invitations.build(params[:invitation])
end
end
Feedback?
I’m feeling pretty happy with this idea, so I’m thinking I’ll keep the code around and maybe throw a small gem up on GitHub.
UPDATE: The gem’s now in my GitHub account as default_association_attributes.
I may have missed another solution though, so I’d love to hear from you—how have you solved this problem? How have you felt about your solution?
-
Having Invitation hang onto its own email address allows it to be re-used in non-Subscriber contexts—like when a User wants to create an Invitation for a friend.
↩ -
Somehow, using a custom method like
↩Subscriber#build_invitation_with_emaildoesn’t feel much better—then I have to remember to use it instead of the default.
Using Authlogic and ActiveResource
Out of the box, Authlogic identifies the current user by looking for four different values:
- The user’s “single access token” in the request parameters.
- The user’s id in the session.
- The user’s “persistence token” in a remember me cookie.
- The user’s login and password in an HTTP basic authentication header.
This is pretty awesome—most things you’d like to do Just Work.
But I’d like to make one small adjustment, since I’m using ActiveResource and following the example of the Highrise API: rather than doing HTTP basic authentication with the user’s login and password, I’d like to use an API key instead.
It turns out Authlogic makes this fairly easy:
class UserSession < Authlogic::Session::Base
# Adjust how Authlogic identifies the current user.
# The default setting is :params, :session, :cookie, :http_auth.
find_with :session, :cookie, :api_key
def valid_api_key?
controller.authenticate_with_http_basic do |api_key, _|
self.unauthorized_record = search_for_record("find_by_#{single_access_token_field}", api_key)
self.persisting = false
self.valid?
end.tap do |authenticated|
self.persisting = true unless authenticated
end
end
end
So now I can write a Widget Resource class like this:
class Widget < ActiveResource::Base
self.site = 'http://localhost:3000/'
self.user = 'MY_API_TOKEN'
end
Or poke around with curl:
# Create a new, empty Widget.
curl --user 'MY_API_TOKEN:X' \
--header 'Content-Type: application/xml' \
--data '<widget></widget>' \
http://localhost:3000/widgets.xml
