Monday, January 30, 2012

Pennants Afghan pattern

I finished a major knitting project and wrote up the pattern for it as a PDF, licensed under Creative Commons.
We've agreed to share the blanket at home, where it will live on the couch:
sleeping boy and blanket Mom, cat and blanket
I'll be posting the pattern to Ravelry too, several commenters there have asked for it based on seeing my project photos.

Given I'm a wizard

I'm working on a Rails Web site (ShareTheVisit) and using a gem called Sorcery. I like the idea behind Sorcery, as a library that offloads the common features around picking passwords, activating accounts and so on. The Railscast on Sorcery really helped too. However, I've run into a few snags.

The major snag is the automation that Sorcery does on the object you designate for the user object. For example, if you tell Sorcery that a "User" instance is what it logs in, then the User account needs a password according to Sorcery. If you also tell Sorcery that you wish to use the email activation feature, then it does its magic every time a User object is created.

What's the problem with this? Our use case required me to create some information around a user who hadn't joined the site yet. E.g. I'm inviting my doctor to the site, and when she joins, I need her account associated with mine. For better or for worse, I decided to do this by creating a model for the invited doctor, but it couldn't be a User instance because that needs a password which we're not going to go choosing for the doctor, and even if we let the doctor choose the password later, creating the User account too early makes the email activation happy too early. So instead of creating a User instance when the doctor is invited, I created an Account model for the doctor. When the doctor comes to the site and chooses a password, then I create the User instance, associated it with the Account, and the activation email is sent by Sorcery.

So far so good, but the email activation is still a problem around invitations. If somebody comes to the site and registers ad-hoc, we want Sorcery to send an email before activating the account, so that we are certain that email will work for resetting passwords and such. But if somebody clicks on a link in an invitation they received in email, doesn't that validate the email address even more smoothly? So I'd like to skip the activation-required email depending on whether the registration form came with a token indicating the User was invited via a working email address. No luck doing this directly, Sorcery does its magic whether I'd like it to or not, when the User account is created. I hacked around it like this, setting the email initially to something bogus that will fail to be delivered, before resetting the email after the save event.


  def register(account, params)
    user = User.new(params[:user])
    user.account = account
    if ValidEmailToken.exists?(:email => account.email, :token => params[:token])
      user.email = "#{params[:token]}+temporary@example.com"  #Temporary -- make the "activation required" email fail to be sent
      if user.save
        user.activate!
        auto_login(user)
        user.email = account.email
        user.save
        AdminMailer.user_registered_notify_email(user).deliver
      end
    else
      user.email = account.email
      if user.save
        AdminMailer.user_registered_notify_email(user).deliver
      end
    end
    return user
  end

Ugh. Even uglier, I had to put this in ApplicationHelper rather than in either the User or Account model as I would have liked. Sorcery only makes auto_login available to controllers (and ApplicationHelper is a mixin to controllers). I'm going to have to totally refactor this when I figure out how. I'd like to reunite the User and Account objects or at least reduce the redundancy -- because of the path by which I got this working, an Account has an email address when a new person is invited by an existing user, and a User has an email field too because that's what Sorcery needs.

Another snag came in automated functional testing. I'm using Cucumber and Capybara. I write tests like this:


 Scenario: Creating a visit with a new patient but no contact info
    Given I'm a doctor
    When I am attempting to create a new visit
    And I can add a patient
    And I click Create Visit
    Then I should see an error with "Need contact information"

To make this work I need to write the code behind each step, including "Given I'm a doctor".


 Given /^I'm a doctor$/ do
    @user = User.create!(:email => "testuser@example.com", :password => "password")
    DoctorsController.any_instance.stub(:current_user).and_return(@user)
    VisitsController.any_instance.stub(:current_user).and_return(@user)
    PatientsController.any_instance.stub(:current_user).and_return(@user)
  
    @doctor = Doctor.create!(:first_name => "Gregory", :last_name => "House", :degree =>"MD", :user => @user)
  end

This shows how I had to stub out the magic Sorcery method "current_user" in each controller, because Sorcery does something like (I'm guessing, and I'm too lazy to read the Sorcery code to confirm) add methods to each actual controller. I definitely tried stubbing out "current_user" in the ApplicationController, which each other controller extends, but that just did not work.

I hope these tips help somebody else make Sorcery work for them, or to choose something else; or that somebody can let me know if I should be doing something different. I'm coding by myself these days so when I ask myself, "Self, is this a good idea?" I don't usually get a very good answer. It's quite probable I'm overlooking some decently obvious improvements to my situation.

To fully unpack the title of this blog post, "Given I'm a wizard"? The keyword "Given" is how Cucumber, in its domain-specific language, expresses pre-requisites, as in the example above. Obviously I'm a wizard if I'm using Sorcery, plus "In the basement rolling dice... I'm a wizard!" And yes, in the D&D campaign I'm currently playing in, I'm a sorcerer.

Blog Archive

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 Unported License.