Making “as_string” Attribute Readers for ActiveRecord

Posted by Brian in Howto, Metaprogramming, Rails, tips (March 15th, 2010)

Occasionally, I need to transform boolean model attributes like “active” to display “active” or “inactive” instead of “true” or “false” when making reports or views. A lot of times this means writing some kind of helper method like this:

def active_or_inactive(object, true_message, false_message)
  object.active ? true_message : false_message
end

and calling it like this:

  <%= active_or_inactive(@project, "Active", "Inactive" %>

That’s not a bad approach, and it helps keep the views slightly cleaner by keeping the logic out, but it ends up being more characters than simply using a ternary operator in the view. I’ve used a slightly different approach in some of my more recent projects and I thought I should share it with you.

Move It To The Model

That’s right, I’m advocating pushing that helper into the model itself. I can hear you now, yelling something about “this guy doesn’t know what he’s talking about! How dare he put display logic in his models!” But before you close your browser, allow me to explain.

It just so happens that I need this logic not only in my views, but in my text-based reports that I run outside of the web server. I could mix the module with the helpers in when I needed it, but there’s also something un-object-oriented that bugs me about helpers. They remind me of PHP a bit. I feel like I should be calling object.active_as_string("Active", "Inactive") instead. So that’s what I’m going to do.

First, a unit test, because we’re all good professionals that write tests first. I want to call a method called active_as_string which takes two parameters – the string to print when it’s true and the string to print when it’s
false. Here are my tests:

require 'test_helper'

class ProjectTest < ActiveSupport::TestCase

  test "should display 'Active' if active" do
    p = Project.new(:active => true)
    assert_equal p.active_as_string("Active", "Inactive"), "Active"
  end

  test "should display 'Inactive' if not active" do
    p = Project.new(:active => false)
    assert_equal p.active_as_string("Active", "Inactive"), "Inactive"
  end
end

Tests help me design the method’s use up front. With two failing tests as my guide, I can now take my first stab at making the method work:

class Project < ActiveRecord::Base
   def active_as_string(true_message, false_message)
      self.active ? true_message : false_message
   end
end

With that implemented, my tests pass. However, I also have a "closed" boolean I need to handle, and it would also be nice if I could display "No description" if a project's description was blank. I could write my own _as_string methods like I've done already, but instead, I'll do a little metaprogramming to generate what I need.

Let's add four more test cases - to test the "closed" and the "description" fields.

  test "should display 'Closed' if closed" do
    p = Project.new(:closed => true)
    assert_equal p.closed_as_string("Closed", "Open"), "Closed"
  end

  test "should display 'Open ' if not closed" do
    p = Project.new(:active => false)
    assert_equal p.closed_as_string("Closed", "Open"), "Open"
  end
  
  test "should display 'No Description' if description is nil" do
    p = Project.new(:description => nil)
    assert_equal p.description_as_string("No Description"), "No Description"
  end

  test "should display the description if it exists" do
    p = Project.new(:description => "Hi there!")
    assert_equal p.description_as_string("No Description"), "Hi there!"
  end

Now, let's build some methods!

ActiveRecord::Base.columns

Every ActiveRecord class has a class method called columns that returns a collection of column objects. The Column object describes each database column and lets you determine its type and its name. We can use that and class_eval to generate a whole bunch of methods at runtime.


class Project < ActiveRecord::Base
  self.columns.each do |column|

    if column.type == :boolean

      class_eval <<-EOF

        def #{column.name}_as_string(t,f)
          value = self.#{column.name}
          value ? t : f
        end

      EOF

    end
  end
end

In this example, we're creating the _as_string method for each boolean column. It takes two parameters and is basically the same code we already used in our original method earlier. Notice how class_eval can do string interpolation using Ruby's #{} syntax. That makes it easy to build up the method names.

We can use that same concept to do the same for any other methods - we'll just cast them to strings and check to see if they are blank.

  class_eval <<-EOF

    def #{column.name}_as_string(default_value)
     value = self.#{column.name}.to_s
     value.blank? ? default_value : value
    end

  EOF

We throw that into the else block and our whole example looks like this:

  class Project < ActiveRecord::Base
  
    self.columns.each do |column|
    
      if column.type == :boolean
      
        class_eval <<-EOF
        
          def #{column.name}_as_string(t,f)
            value = self.#{column.name}
            value ? t : f
          end
          
        EOF
        
      else
      
      class_eval <<-EOF
      
          def #{column.name}_as_string(default_value)
           value = self.#{column.name}.to_s
           value.blank? ? default_value : value
          end
          
        EOF
        
      end
      
    end
  end

If you run your tests now, they all pass. But our work isn't done - this isn't very DRY. We may want to use this in another class too.

Modules!

Create a new module and mix the behavior into your models. Create the file lib/active_record/as_string_reader_methods.rb (create the active_recordfolder if it doesn't exist already) and put this code in the file:

  module ActiveRecord
    module AsStringReaderMethods
     def self.included(base)
       create_string_readers(base)
     end

     def self.create_string_readers(base)
      base.columns.each do |column|

         if column.type == :boolean

           class_eval <<-EOF

             def #{column.name}_as_string(t,f)
               value = self.#{column.name}
               value ? t : f
             end

           EOF
         else

           class_eval <<-EOF

             def #{column.name}_as_string(default_value)
               value = self.#{column.name}.to_s
               value.blank? ? default_value : value
             end

           EOF
         end
       end
     end
    end
  end

It's mostly the same code we had before, but in this case we're using the self.included method to trigger the method creation on the model that includes the module.

Now, remove the code from your Project mode and replace it with

include AsStringReaderMethods

Run your tests, and everything should pass. You now have a module you can drop into your projects and you'll have this functionality yourself. Now it's up to you to expand upon this, and use this pattern in your own work if you find it useful.

Good luck!

Basic Authentication in Ruby on Rails – In Case You Forgot

Posted by Brian in News (March 11th, 2010)

There are so many authentication choices in Rails these days, but sometimes the simplest approach is the best approach. I’m working with a client right now building an application that has a mostly-public interface. Only a handful of people need to log in to the site, and they only need to modify content occasionally. It’s not a complicated project at all, and while my first instinct was to reach for my starter project that I usually use for these kinds of things, I thought again and realized the following:

  1. This project doesn’t need to let people sign up.
  2. There’s no need to send password recovery instructions
  3. Much of the data entry will be done with a mobile device connecting to the app’s REST-style XML API.

Something like Authlogic, or even Restful Authentication seems like overkill for something this simple. Many Rails developers are probably used to their solutions because many Rails projects are more complex than this. In the spirit of keeping things simple, I’m going to show you how to do authentication of users without any plugins, the way we used to do it in 2005. (For those of you that have been working with Rails as long as me, this will be a good refresher for you. When was the last time you hand-rolled your authentication solution?)

Back To Basic (Authentication, that is)

Since Rails 2.0, Basic Authentication has been an option, as Ryan Bates explains in Railscast #82. We’ll use that and a basic user model to authenticate our users.

First, generate a user mode with login and a hashed password fields. I’m not going to do any salting here, as it’s not necessary. If you want it, you should be able to add it easily.

ruby script/generate model User login:string hashed_password:string

Now we need to modify the User model to do the password encryption.

First, set up the validations and the attribute accessors for the password and password_confirmation fields.

  validates_presence_of :login
  validates_confirmation_of :password
  attr_accessor :password

Next, be a good developer and write a unit test for hashing the password.

  test "should create a user with a hashed password" do
    u = User.create(:login => "homer",
                 :email =>"homer",
                 :password => "1234",
                 :password_confirmation => "1234")
    u.reload # (make sure it saved!)            
    assert_not_nil u.hashed_password
  end

Prepare your test database

rake db:test:clone

and run your test

rake test:units

With the test in place, write the code to encrypt the password on save. Add the SHA1 digest library at the top of your class:

require 'digest/sha1'

Then add the before filter and an encryption method:

  before_save :encrypt_password
 
  def encrypt_password
    unless self.password.blank?
      self.hashed_password = Digest::SHA1.hexdigest(self.password.to_s)
      self.password = nil
    end
    return true 
  end

Run your test again and everything should pass.

rake test:units

Authenticating Users

Now we need to write a class method that we can use to grab a user from the database by looking up their username and hashed password. We’ll use a simple pattern for this. First, let’s write a quick test:

  test "given a user named homer with a password of 1234, he should be authenticated with 'homer' and '1234' " do
    User.create(:login => "homer",
                 :email =>"homer",
                 :password => "1234",
                 :password_confirmation => "1234")
    assert User.authenticated?("homer", "1234")
  end

We create a user and then call User.authenticated?. If its return value evaluates to True, we’ve got a good set of credentials. Add this class method to your User model to make your test pass:

  def self.authenticated?(login, password)
    pwd = Digest::SHA1.hexdigest(password.to_s)
    User.find_by_login_and_hashed_password(login, pwd)
  end

Notice that here, I’m actually returning the user object, rather than a boolean. If no user is found, nil is returned which evaluates to false. Remember, in Ruby, everything except nil and false evaluates to True.

Our entire user model looks like this:

require 'digest/sha1'
class User < ActiveRecord::Base
  
  validates_presence_of :login
  validates_confirmation_of :password
  attr_accessor :password
  
  before_save :encrypt_password
  
  def self.authenticated?(login, password)
    pwd = Digest::SHA1.hexdigest(password.to_s)
    User.find_by_login_and_hashed_password(login, pwd)
  end
  
  private
  
  def encrypt_password
    unless self.password.blank?
      self.hashed_password = Digest::SHA1.hexdigest(self.password.to_s)
      self.password = nil
    end
    return true 
  end
end

Creating the Filter

Let's create a simple Projects scaffold. We'll use our authentication to protect this scaffolded interface.

ruby script/generate scaffold Project name:string description:text completed:boolean

Now, open app/controllers/application_controller.rb and this code:

  def authenticate_with_basic_auth
    authenticate_or_request_with_http_basic do |username, password|
      @current_user = User.authenticated?(username, password)
    end
  end

This tiny bit of code will pop up a Basic Authentication credentials box and look the user up in our database using the supplied credentials. If we find our user, we put it in the @current_user instance variable. It's common practice in Rails apps to have a current_user helper method, so we can add that to application_controller.rb as well.

  helper_method :current_user

  def current_user
    @current_user
  end

With that, we simply need to invoke the filter. Open your projects_controller.rb file and add this to the top:

before_filter :authenticate_with_basic_auth

And that's it! You've protected the application. Create a user via the Rails runner, fire up script/server and test it out!

ruby script/runner 'User.create(:login => "homer", :password => "1234", :password_confirmation => "1234")
ruby script/server

You should also be able to use cURL to play with the XML REST-style API provided by the scaffold generator.

Get projects:

curl http://localhost:3000/projects.xml -u homer:1234

Create project via XML

curl http://localhost:3000/projects.xml \
-u homer:1234 \
-X POST \
-d "Test" \
-H "Content-Type: text/xml"

Simple is Good

This simple solution is easy to write, easy to maintain, and easy to extend. It's also something that anyone with any practical experience with Rails should be able to write in as much time as it would take to configure Authlogic.

So what's left to do with this? First, SHA1 isn't great encryption - it's just hashing. BCrypt might be better, Adding a salt to this hash might be another good idea too. Some more tests would be great. So, go write them, make this fit your security needs, and have fun! As always, I'd love to hear your comments.