Showing posts with label rails. Show all posts
Showing posts with label rails. Show all posts

27 March, 2008

New Gem: Avatar

I released Avatar version 0.0.3 today. This gem offers avatar support for a variety of sources. It's not Rails-specific, but to use it in a Rails app, do something like the following.

In app/helpers/people_helper.rb:

class PeopleHelper
  include Avatar::View::ActionViewSupport

  def default_avatar_url(size)
    req = controller.request
    "#{req.protocol}#{req.host_with_port}#{image_path("/images/avatar_default_#{size}.png")}"
  end
end

app/views/people/show.html.erb:

<%= avatar_tag(@current_user, :size => 40, :default => default_avatar_url(:small) %>

The default settings will check for a Gravatar for @current_user.email. There are other implementations, including one that works with the file_column plugin. I'll be happy to add more implementations; the project is hosted on GitHub.

01 March, 2008

The Final Word on Rails Association Extension

There has been a great deal of debate on how to extend Rails Associations. Ryan discusses the "block" method:

class Organization < ActiveRecord::Base
  has_many :people do
    def find_active
      find(:all, :conditions => ["active = ?", true])
    end
  end
end

This is identical to the "extend" method; in fact Rails converts a "block" into an "extend":

module FindActiveExtension
  def find_active
    find(:all, :conditions => ["active = ?", true])
  end
end

class Organization < ActiveRecord::Base
  has_many :people, :extend => FindActiveExtension
end

The commenters then debate the merits of the "class method" method:

class Person < ActiveRecord::Base
  def self.find_active
    find(:all, :conditions => ["active = ?", true])
  end
end

class Organization < ActiveRecord::Base
  has_many :people
end

In all three cases (really two), you can call something like:

organization.people.find_active

There are differences, though! I ran some experiments to find out how each works, and found that unless you have some particular reason not to, you should always use the "block" or "extend" version, for two reasons:

  1. The "class method" version does not allow manual caching.
    class Person < ActiveRecord::Base
      def self.find_active
        @active ||= find(:all, :conditions => ["active = ?", true])
      end
    end
    
    Will break, as Adam T. mentioned in the comments.
  2. The association version is slightly faster then the class version. I created the same has_many relationship to two pairs of classes (Person has_many Things; User has_many Widgets). I then added 6 sub-association selectors (red, green, blue, small, medium, large) in different ways: Thing had class methods; User had the same methods embedded in the has_many association. I then ran benchmarks on 100 users/people and 5000 things/widgets (with identical respective associations). The x1 times are for finding all of 6 sub-association lookups on every Person (Class) and User (Association). The x2 times are for finding and reloading all 6 (that is, to gain the benefit of any automatic caching done by Rails or the database). In absolute times:
                               user     system      total        real
    Class (x1):            4.650000   0.680000   5.330000 ( 11.320903)
    Association (x1):      4.530000   0.670000   5.200000 ( 10.861711)
    Class (x2):            9.240000   1.360000  10.600000 ( 21.934539)
    Association (x2):      9.000000   1.330000  10.330000 ( 21.066327)
    
    Dividing the bottom two rows in half:
                               user     system      total        real
    Class (x1):            4.650000   0.680000   5.330000 ( 11.320903)
    Association (x1):      4.530000   0.670000   5.200000 ( 10.861711)
    Class (x2)/2:          4.620000   0.680000   5.300000 ( 10.967270)
    Association (x2)/2:    4.500000   0.665000   5.165000 ( 10.533164)
    
    Dividing all rows by the number of queries performed (per/query time):
                               user     system      total        real
    Class (x1)/600:        0.007750   0.001133   0.008883 (  0.018868)
    Association (x1)/600:  0.007550   0.001117   0.008667 (  0.018103) 
    Class (x2)/1200:       0.007700   0.001133   0.008833 (  0.018279)
    Association (x2)/1200: 0.007500   0.001108   0.008608 (  0.017555)
    
    The association version is a little faster, and gains more from the automatic caching Rails does.
  3. You can check out the experiment at https://svn.u-presence.com/svn/experiments/association_vs_class/ (username guest, no password).

19 February, 2008

with_modules_unavailable and some Module helpers

I've been trying to test a new Rails plugin (stay tuned!), and I've found that I need to have certain Modules unavailable. This is useful when you need to test that missing Modules will raise errors or if you have behavior conditional on what gems are installed.

I wanted something like:

...
with_modules_unavailable(Foo, Bar::Baz::Goo) do
  test stuff
end

To get this working, just put the following in your test_helper.rb (this version requires Inflector):

Module.class_eval do
  
  def defining_module
    chain = self.to_s.split(/\:\:/)
    chain.pop
    if chain.empty?
      Object
    else
      chain.join('::').constantize
    end
  end
  
  def simple_name
    self.to_s.split(/\:\:/).pop
  end
  
end

Test::Unit::TestCase.class_eval do

  def with_modules_unavailable(*mods, &block)
    Thread.exclusive do
      mods.each do |mod|
        mod.defining_module.send :remove_const, mod.simple_name.to_sym
      end
      yield block
      mods.reverse.each do |mod|
        mod.defining_module.send :const_set, mod.simple_name.to_sym, mod
      end
    end
  end
  
end

You can test that it works with:

def test_with_modules_unavailable
  with_modules_unavailable(ActiveRecord::Base) do
    assert_raise(NameError) { ::ActiveRecord::Base.class_eval { } }
  end
  assert_nothing_raised { ::ActiveRecord::Base.class_eval { } }
end

14 February, 2008

ActiveSupport additions: TimeRange and Distance

I've created some utility classes that I use in many of my projects. I've broken them out into a Rails plugin, but they should really be part of ActiveSupport.

First: TimeRange

t = 5.minutes.ago.until(1.second.ago)
s = 10.minutes.ago.until(3.minutes.ago)
overlap = t & s
overlap.start_time # => 5.minutes.ago
overlap.end_time   # => 3.minutes.ago

Second: Distance

4.miles + 6.miles  # => 10.miles
5.yards.to_feet    # => 15.feet
12.miles.in_km     # => 19.312128.km
12.miles.as_km     # => 19.312128.km
2.meters > 2.yards # => true

Eventually, I plan on moving some of GeoKit into some sort of ActiveRecord::DistanceSupport in the plugin.

If you want the plugin, you can get it at https://svn.u-presence.com/svn/plugins/active_support_hacks/ (Username guest, no password). I'd love any other suggestions or comments on the utilities.

09 February, 2008

Hacking Rubygems' #require, #require_gem, #gem

I have some old gems in my latest Rails app, and Rubygems has changed its syntax since those gems were built. The gems have require_gem in them, but the newest version of Rubygems doesn't add that command to Kernel, so you get NoMethodErrors when the gem loads. This simple hack in config/environment.rb will fix it:

# hack rubygem's change to #require:
Kernel.class_eval do
  def require_gem(*args)
    gem *args
  end
end
I have it right before require File.join(File.dirname(__FILE__), 'boot') in case my config/environments/xxx.rb loads an old gem.

06 July, 2007

has_many_polymorphs and the open-closed principle

I like has_many_polymorphs. I would love it if only it obeyed the open-closed principle.

Let's say I have a PetOwner model, and I want it to have_many pets polymorphically.

has_many_polymorphs lets you do this:

class PetOwner < ActiveRecord::Base
  has_many_polymorphs :from => [:dogs, :cats, :fish]
  ...
end

But what if I later decide I want to add a Ferret class? In addition to

class Ferret < AbstractPet
  ...
end

I also have to change PetOwner:
class PetOwner < ActiveRecord::Base
  has_many_polymorphs :from => [:dogs, :cats, :fish, :ferrets]
  ...
end

It would be really lovely if has_many_polymorphs could just tell what has inherited from AbstractPet. There's no reason you can't do this (in fact, I did before I wrote this post), but there's a timing problem: all of the inherited classes have to load before PetOwner does. The has_many_polymorphs ... call only happens when PetOwner is first included, so all of those other classes have to have already registered themselves with AbstractPet (simply by inheriting from it) by then; unfortunately, there's no good way to guarantee class load order without major hacking at the environment.

Sometimes we just have to put up with not-the-best. Alas.

01 July, 2007

and the test_spec_on_rails journey continues

Test/Spec has a wonderful - if largely unnoticed - feature: you can specify the superclass of contexts. The default is the ole' Test::Unit::TestCase, but that won't always do.

Let's say, hypothetically, you wanted to write some integration tests for your not-quite-shiny Rails app. You'd use ApplicationController::IntegrationTest, right? That way you can do things like get "/posts/34.html" and it would look up the routings and just do the right thing. Awesome.

Except... things aren't quite so simple when using test/spec. Therefore, I bring you an illustrious, illustrative example:

require File.dirname(__FILE__) + '/../test_helper'

class UserStoriesTest < ActionController::IntegrationTest
 fixtures :people, :openid_authentications, :password_authentications
 context "User Stories", ActionController::IntegrationTest do
   context "a person coming to the site to log in", ActionController::IntegrationTest do
     specify "should see the welcome page" do
       get '/'
       template.should.be 'welcome/index'
       status.should.be 200
     end
     specify "should be able to successfully log in with email and password..." do
       post_via_redirect '/login.html', {:email => 'pete.thomas@xahoo.com', :password => 'test'}
       template.should.be 'people/home'
       status.should.be 200
     end
   end
 end

end

test_spec_on_rails continued...

Yesterday, I was very happy because all my controller tests (using test/spec, of course) were passing individually.

Today I decided to run rake test:functionals just to make sure they played well together. CRASH! BOOM! OTHER LOUD NOISES!

The problem, after two hours of debugging, is that I was creating the same context in different controllers. DocumentationControllerTest had a "A guest" context, and so did "AccountControllerTest" and "WelcomeControllerTest"

Test/spec is perfectly happy to merge these . . . I just didn't realize it merged them. I assumed that it would scope the context to its parent TestClass. No such luck.

The solution is quite easy, and really not all that bad practice anyway: scope your contexts manually. For example:

class WelcomeControllerTest < Test::Unit::TestCase
 context "The Welcome Controller" do
   context "A guest" do
     ...
   end
   context "A logged-in user" do
     ...
   end
 end
end

30 June, 2007

ActsAsAuthenticated in test_spec_on_rails

I was having trouble getting "login_as" from the ActsAsAuthenticated plugin to work with test/spec on Rails.

TechnoWeenie suggested doing a def login_as... within each context. (It no longer does; I've put the code from this post into that page now.) That didn't seem very DRY, so I tried including ActsAsAuthenticated all over the place. Nothing worked...

Until I realized the problem: each "context" block creates a new instance of Test::Unit::TestCase. I had assumed it created a new instance of XxxControllerTest. Alas. Easy to fix. Just putTest::Unit::TestCase.send(:include, AuthenticatedTestHelper) before the opening of your XxxControllerTest.

For example:

require File.dirname(__FILE__) + '/../test_helper'
require 'welcome_controller'

Test::Unit::TestCase.send(:include, AuthenticatedTestHelper)

# Re-raise errors caught by the controller.
class WelcomeController; def rescue_action(e) raise e end; end

class WelcomeControllerTest < Test::Unit::TestCase
  fixtures :people
  def setup
    @controller = WelcomeController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end
  context "A logged-in person" do
    setup do
      use_controller WelcomeController
      login_as :yvette_moore
    end
     ...
  end
end