Refactoring an ActiveRecord callback

Inspired by a few articles and pesentations (1 2 3 4), I decided it was time to cleanup some of the logic in my Post model related to a particular ActiveRecord callback. The fact that I needed some comments to explain what it is doing should be a red flag.

before_validation :update_published_at_if_necessary

def published?
  self.is_published == true
end

def unpublished?
  ! published?
end

protected
  # Ensure that published_at is set accordingly.
  # 
  #  * Unpublished posts should not have this set
  #  * Published posts should have it set to the current time
  def update_published_at_if_necessary
    if new_record? && published? && published_at.nil?
      self.published_at = Time.now
    end
    if ! published? && !published_at.nil?
      self.published_at = nil
    end
  end

Fortunately, I have tests in place that excercise this logic, so as long as my tests are passing, the refactorings must work (hopefully!).

My first impression is that the callback is doing too much work. Let's split it up into two pieces.

before_validation :set_published_at_to_now_if_necessary
before_validation :unset_published_at_if_necessary

protected
  def set_published_at_to_now_if_necessary
    if new_record? && published? && published_at.nil?
      self.published_at = Time.now
    end
  end

  def unset_published_at_if_necessary
    if ! published? && !published_at.nil?
      self.published_at = nil
    end
  end

That's somewhat better. At least the methods are more focused.

Hmm, I probably don't need to check the existing published_at value when something isn't published. I can also make use of unpublished?

def unset_published_at_if_necessary
  if unpublished?
    self.published_at = nil
  end
end

I would do something similar for set_published_at_if_necessary, but I don't want to override the published_at if it was explicitly set. Maybe I post something in the future, or past. I go a little crazy sometimes with that.

I could probably simplify the conditional logic in set_published_at_if_necessary by making a new method.

protected
  def set_published_at_to_now_if_necessary
    if new_published_post_without_published_at?
      self.published_at = Time.now
    end
  end

  def new_published_post_without_published_at?
    new_record? && published? && published_at.nil?
  end

Having if_necessary into the method names are kind of bugging me. before_filter supports :if and :unless options, so we should use those, and remove the conditionals from the callback methods.

before_validation :set_published_at_to_now, :if => :new_published_post_without_published_at?
before_validation :unset_published_at, :if => :unpublished?

protected
  def set_published_at_to_now
    self.published_at = Time.now
  end

  def unset_published_at
    self.published_at = nil
  end

Looking good. Looking pretty, pretty good.

Let's see the finished product:

before_validation :set_published_at_to_now, :if => :new_published_post_without_published_at?
before_validation :unset_published_at, :if => :unpublished?

def published?
  self.is_published == true
end

def unpublished?
  ! published?
end

protected
  def set_published_at_to_now
    self.published_at = Time.now
  end

  def unset_published_at
    self.published_at = nil
  end

  def new_published_post_without_published_at?
    new_record? && published? && published_at.nil?
  end

I'm pretty happy with this. Reads really well. Some of the methods have kind of long names, but I can deal with that.

The only part I don't really like is published? and unpublished?, but that's for another day.

July 03, 2008 at 22:13 Permalink Edit Destroy

A walk through of test-driven development with shoulda

I'm not sure if you've heard, but shoulda makes test-driven development really simple.

Last week's boston.rb focused on testing, where we split up into two groups. One group worked on adding a test suite to an app that had no tests, and my group was using test-driven development for adding new features to the boston.rb website.

I figured I'd do recap how we used TDD that night.

shoulda-generators, a quick plug

One thing I'm going to omit from the walkthrough is that a chunk of time was spent converting the scaffold-generated test from test-unit to shoulda, and from erb to haml. While it was a worthwhile detour for some present, it was a little annoying. If only there were shoulda and haml generators, I say!

Struck awake by inspiration at 5am the next day, I took fate into my own hands...

BEHOLD!

It is available as a plugin for now, but I'll try to gemify it sometime.

While we didn't have the luxury of these generators at the time, I'm going to use them now for the sake of this writeup.

The new feature: Places

Before we start, let's just make sure the tests work... alright, good.

We want to add functionality for 'places.' A Place is somewhere in the area that's somehow connected to Ruby: a business using it, a venue for a meeting, an incubator, things like that.

A Place probably has:

  • A name
  • An address
  • A description

Let's generate the scaffold:

$ script/generate shoulda_scaffold place name:string address:string description:text
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/places
      exists  app/views/layouts/
      exists  test/functional/
      exists  test/unit/
      create  public/stylesheets/blueprint
      create  app/views/places/index.html.haml
      create  app/views/places/show.html.haml
      create  app/views/places/new.html.haml
      create  app/views/places/edit.html.haml
      create  app/views/places/_form.html.haml
      create  app/views/layouts/places.html.haml
      create  public/stylesheets/blueprint/print.css
      create  public/stylesheets/blueprint/screen.css
      create  public/stylesheets/blueprint/ie.css
      create  app/controllers/places_controller.rb
      create  test/functional/places_controller_test.rb
      create  app/helpers/places_helper.rb
       route  map.resources :places
  dependency  shoulda_model
      exists    app/models/
      exists    test/unit/
      create    test/factories/
      create    app/models/place.rb
      create    test/unit/place_test.rb
      create    test/factories/place.rb
      create    db/migrate
      create    db/migrate/20080620011343_create_places.rb

Let's try the tests again.

$ rake test
(in /Users/nichoj/Projects/bostonrb)
You have 1 pending migrations:
  20080620011343 CreatePlaces
Run "rake db:migrate" to update your database then try again.

Whoops, need to migrate first.

$ rake db:migrate
(in /Users/nichoj/Projects/bostonrb)
== 20080620011343 CreatePlaces: migrating =====================================
-- create_table(:places)
   -> 0.0171s
==
$ rake test
(in /Users/nichoj/Projects/bostonrb)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/unit/place_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
Started
...
Finished in 0.147509 seconds.

3 tests, 3 assertions, 0 failures, 0 errors
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb" "test/functional/places_controller_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
Started
...........
Finished in 0.293275 seconds.

11 tests, 14 assertions, 0 failures, 0 errors
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb"

Good so far.

Now what do we want to do with the place? Those attributes should be required. Open the test, and see what we have so far:

class PlaceTest < ActiveSupport::TestCase
  should_have_db_column :name
  should_have_db_column :address
  should_have_db_column :description
end

Oh, cool. The generated test already has a few shoulds in there. Now we want to add should_require_attributes.

class PlaceTest < ActiveSupport::TestCase
  should_have_db_column :name
  should_have_db_column :address
  should_have_db_column :description
  should_require_attributes :name, :address, :description
end

And let's see how this does...

$ ruby test/unit/place_test.rb
Loaded suite test/unit/place_test
Started
...FFF
Finished in 0.154072 seconds.

  1) Failure:
test: Place should require address to be set. (PlaceTest)
    [/Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb:41:in `__bind_1213925922_190232'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `call'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `test: Place should require address to be set. '
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run']:
Place does not require address.
<false> is not true.

  2) Failure:
test: Place should require description to be set. (PlaceTest)
    [/Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb:41:in `__bind_1213925922_194386'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `call'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `test: Place should require description to be set. '
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run']:
Place does not require description.
<false> is not true.

  3) Failure:
test: Place should require name to be set. (PlaceTest)
    [/Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb:41:in `__bind_1213925922_196154'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `call'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `test: Place should require name to be set. '
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run']:
Place does not require name.
<false> is not true.

6 tests, 6 assertions, 3 failures, 0 errors

Ok, this is to be expected. We didn't actually validate_presence_of the fields. Let's do that now:

class Place < ActiveRecord::Base
  validates_presence_of :name
  validates_presence_of :address
  validates_presence_of :description
end

If we try it again...

$ ruby test/unit/place_test.rb
Loaded suite test/unit/place_test
Started
......
Finished in 0.153653 seconds.

6 tests, 12 assertions, 0 failures, 0 errors

Test-driven, wooo!!! I think this is good enough to commit.

Relating places and events

We already have an Event model, so I figure a Place should be able to have events, and event would have could be at a particular place. Shall we test it?

class PlaceTest < ActiveSupport::TestCase
  # omitted...
  should_have_many :events
end

We get a failure though:

  1) Failure:
test: Place should have many events. (PlaceTest)
    [/Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb:378:in `__bind_1213931312_854475'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `call'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `test: Place should have many events. '
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run']:
Place does not have any relationship to events.
<nil> is not true.

Now we add the has_many to Place. But we still have a failure!

 1) Failure:
test: Place should have many events. (PlaceTest)
    [/Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb:405:in `__bind_1213931441_769681'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `call'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `test: Place should have many events. '
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run']:
Event does not have a place_id foreign key.
<false> is not true.

Oh, probably should migrate something.

$ script/generate migration place_event_relationship
      exists  db/migrate
      create  db/migrate/20080620031448_place_event_relationship.rb

And we make it look like:

class PlaceEventRelationship < ActiveRecord::Migration
  def self.up
    add_column :events, :place_id, :integer
  end

  def self.down
    remove_column :events, :place_id
  end
end

Do a rake db:migrate db:test:prepare, and we're ready to run it again. Wait for it... wait for it...

$ ruby test/unit/place_test.rb 
Loaded suite test/unit/place_test
Started
.......
Finished in 0.159728 seconds.

7 tests, 15 assertions, 0 failures, 0 errors

Let's add a test for the other side of the relationship:

class EventTest < ActiveSupport::TestCase
  # ... snip ...
  should_belong_to :place
end

Failures, as expected.

 1) Failure:
test: Event should belong_to place. (EventTest)
    [/Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb:476:in `__bind_1213933933_768823'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `call'
     /Users/nichoj/Projects/bostonrb/vendor/plugins/shoulda/lib/shoulda/gem/shoulda.rb:191:in `test: Event should belong_to place. '
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__'
     /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run']:
Event does not have any relationship to place.
<nil> is not true.

And now we actually implement it...

class Event < ActiveRecord::Base
  belongs_to :place
end

And guess what? They pass. We win at things that are winnable, like TDD.

Summary

These have been pretty trivial examples, but it demonstrates how to roll with TDD.

  • Write a test
  • See it fail
  • Write code
  • See the test pass
  • Repeat

It also demonstrates how shoulda lets tersely test things that can be written tersely with Rails.

June 23, 2008 at 02:56 Permalink Edit Destroy

Getting back to my vim roots with MacVim

Back when I got my MacBook last fall, I chose to go with TextMate for Ruby and Rails development, despite being a long vim user.

And it was good. I think it was deserving of the hype it usually receives from Ruby developers on Macs. Except for the paying for software thing... I still feel dirty about that.

Recently a friend ditched TextMate cold turkey in favor of vim, after seeing the cool and useful stuff coworkers could do with it. After seeing it myself, let's just say I'm going back to my roots.

If you're on a Mac, you just have to use MacVim (not to be confused with this MacVim). It's basically like gvim, except for Mac OS. Duh.

There are a lot really small, nice integration points and features I've been noticing.

  • Mac-like text navigation
    • option-left/right for navigating words
    • apple-left/right for start/end of line
    • apple-up/down for start/end of document
  • Shortcuts using the apple key
    • apple-s for saving
    • apple-n for new item
    • apple-a for select all. ggVG always felt annoying for doing this
  • Recent documents
  • Tabbing (apple-t creates a one, apple-w closes it)
  • Command line tool, mvim (similar to the mate command). It seems to aaccept the same options you'd expect of vim and gvim.

Similar to gvim using ~/.gvimrc, MacVim uses ~/.macvimrc. Personally, I've taken to putting everything in ~/.vimrc, and just creating a symlink.

I'm still working on replicating most of the functionality I came to find useful for Ruby development in TextMate, but I will keep you posted.

I'm also vaguely considering doing some screencasts with vim tips/tricks. One thing I'd want though, is a good way to show what keys are being pressed. I remember back in OS9 days, there was a utility for doing this, but I haven't found a modern equivilant.

June 17, 2008 at 13:03 Permalink Edit Destroy

Boston Ruby Group Meeting June 2008 Summary

Yours truly gave a presentation last night titled 'git as a subversion replacement'. I put the slides up on SlideShare for anyone interested.

Joe Ferris also gave a demo/talk about factory_girl, a tool for using factories in your tests. See the original post announcing factory_girl over at GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS.

There was a backchannel going during the meeting. Here are some notes and links gathered it from and overheard elsewhere:

June 11, 2008 at 19:25 Permalink Edit Destroy

Embracing your paranoia with acts_as_paranoid

According to their RDoc, "Overrides some basic methods for the current model so that calling destroy sets a 'deleted_at' field to the current timestamp. ActiveRecord is required."

There doesn't seem to be really good up-to-date documentation on how to actually use it though. Let's take a stab at it.

Installing

First thing is to install it. I typically use piston to manage them. We need the at least version 1.9.x for its git support.

myapp $ cd vendor/plugins
myapp/vendor/gems $  piston import git://github.com/technoweenie/acts_as_paranoid.git
INFO main: Guessing the repository type
INFO main: Guessing the working copy type
INFO main: Checking out the repository
INFO main: Copying from Piston::Revision(git://github.com/technoweenie/acts_as_paranoid.git@1e9144e483524b4f246a1768462eadb22f634f19)
INFO main: Checked out "git://github.com/technoweenie/acts_as_paranoid.git" 1e9144e to "acts_as_paranoid"

Configuring

We now can define a model class to be paranoid:

class Event < ActiveRecord::Base  
  acts_as_paranoid
end

Internally, acts_as_paranoid uses the deleted_at column to track if something is deleted or not. So, we need to make a migration for it:

myapp $ script/generate migration MakeEventParanoid
  exists  db/migrate
  create  db/migrate/012_make_event_paranoid.rb

It should look something like:

class MakeEventParanoid < ActiveRecord::Migration
  def self.up
    add_column :events, :deleted_at, :datetime
  end

  def self.down
    remove_column :events, :deleted_at
  end
end

You can now migrate:

myapp $ rake db:migrate
== 12 AddDeletedAt: migrating =================================================
-- add_column("events", :deleted_at, :datetime)
   -> 0.3596s
== 12 AddDeletedAt: migrated (0.3596s) ========================================

Using

It might be easiest to demonstrate with this snippet from script/console:

>> event = Event.find(:first)
=> #<Event id: 91133883, date: "2008-04-28 00:00:00", description: "The Ezra event tonight has been cancelled.The Ezra ...", created_at: "2008-04-16 13:30:20", updated_at: "2008-05-30 23:43:22", location: "", lng: nil, lat: nil, title: "Meeting", deleted_at: nil>
>> event.destroy!
=> #<Event id: 91133883, date: "2008-04-28 00:00:00", description: "The Ezra event tonight has been cancelled.The Ezra ...", created_at: "2008-04-16 13:30:20", updated_at: "2008-05-30 23:43:22", location: "", lng: nil, lat: nil, title: "Meeting", deleted_at: nil>
>> Event.find(:first)
=> #<Event id: 420662881, date: "2008-04-15 00:00:00", description: "Worked on creating the app for this site, [bostonrb...", created_at: "2008-04-16 13:30:20", updated_at: "2008-05-29 01:19:41", location: nil, lng: nil, lat: nil, title: "Hackfest", deleted_at: nil>
>> Event.find_with_deleted(:first)
=> #<Event id: 420662881, date: "2008-04-15 00:00:00", description: "Worked on creating the app for this site, [bostonrb...", created_at: "2008-04-16 13:30:20", updated_at: "2008-05-29 01:19:41", location: nil, lng: nil, lat: nil, title: "Hackfest", deleted_at: nil>
>> event.deleted_at = nil
=> nil
>> event.save!
=> true
>> Event.find(:first)
=> #<Event id: 420662881, date: "2008-04-15 00:00:00", description: "Worked on creating the app for this site, [bostonrb...", created_at: "2008-04-16 13:30:20", updated_at: "2008-05-29 01:19:41", location: nil, lng: nil, lat: nil, title: "Hackfest", deleted_at: nil>

A synopsis:

  • Find the first event
  • Delete it
  • Find the first event again, and see that it's something else
  • Find with deleted records, to see our original event
  • Unset deleted_at to 'undelete' the orignal event.

Update: Fixed the script/console section. Was using the wrong variable that I used earlier in the IRB session, but wasn't displayed.

May 31, 2008 at 13:27 Permalink Edit Destroy

Playing with heroku

If you've been following things in the Ruby sphere, you might have heard of heroku. In my mind, it provides two major things:

  • Web UI for Rails development
  • Turnkey Rails hosting and deployment

I attended a workshop earlier today giving a shallow dive through Ruby and Rails, where we used heroku as our testbed.

Initial setup

To start, you'll obviously need an account :) It is currently in invite-only beta, but I might be able to swing an invite or two, so drop me a note if you're interested.

Once you have an account setup, you should install their gem.

urmachine $ sudo gem install heroku
Updating metadata for 3 gems from http://gems.rubyforge.org/
...
complete
Successfully installed heroku-0.2
1 gem installed
Installing ri documentation for heroku-0.2...
Installing RDoc documentation for heroku-0.2...

This gives you access to the basic stuff for managing your heroku apps.

Creating an app

urmachine $ heroku create urapp
Enter your Heroku credentials.
Email: joshua.nichols@gmail.com
Password:
Uploading ssh public key
Created http://urapp.heroku.com/

This does a few things:

  • The first time you use any heroku commands, you will be prompted for your heroku email and password. It will also upload your public ssh key (which you need to create ahead of time)
  • Creates a default app, as if you had done rails urapp locally
  • Sticks this in a remote git repository
  • Starts up the app, and makes it accessible at http://urapp.heroku.com

Developing the app

At this point, you have two options for going forward with development.

  • Use the heroku web interface. Not for me.
  • Pull down a copy of your repository, work remotely and periodically push changes. Yes yes yes yes yes.

The heroku command line tool acts as a front to

urmachine $ heroku clone urapp
Initialized empty Git repository in /Users/nichoj/Projects/urapp/.git/
The authenticity of host 'heroku.com (67.202.2.198)' can't be established.
RSA key fingerprint is 8b:48:5e:67:0e:c9:16:47:32:f2:87:0c:1f:c8:60:ad.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'heroku.com,67.202.2.198' (RSA) to the list of known hosts.
remote: Generating pack...
remote: Done counting 56 objects.
remote: Deltifying 56 objects...
remote:  100% (56/56) done
Indexing 56 objects...
remote: Total 56 (delta 14), reused 0 (delta 0)
 100% (56/56) done
Resolving 14 deltas...
 100% (14/14) done
(in /Users/nichoj/Projects/urapp)

You can now enter the urapp directory and hack away.

Deploying

You'd think this be a long section but it isn't.

urmachine $ git commit -a -m "behold my awesome changes."

This would just commit to your local repository though. Now push it.

urmachine $ git push
updating 'refs/heads/master'
  from 6ee014dfd32c5496b0cc4c3c7301e1c47b9f1b2f
  to   ed9a9e46ebeb90f1d288fc31063439938b9f7e00
 Also local refs/remotes/origin/master
Generating pack...
Done counting 44 objects.
Deltifying 44 objects...
 100% (44/44) done
Writing 44 objects...
 100% (44/44) done
Total 44 (delta 7), reused 0 (delta 0)
refs/heads/master: 6ee014dfd32c5496b0cc4c3c7301e1c47b9f1b2f -> ed9a9e46ebeb90f1d288fc31063439938b9f7e00
HEAD is now at ed9a9e4... behold my changes.
NOTICE:  CREATE TABLE will create implicit sequence "posts_id_seq" for serial column "posts.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "posts_pkey" for table "posts"
NOTICE:  CREATE TABLE will create implicit sequence "comments_id_seq" for serial column "comments.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "comments_pkey" for table "comments"
(in /mnt/home/userapps/18228)
== 1 CreatePosts: migrating ===================================================
-- create_table(:posts)
   -> 0.0361s
== 1 CreatePosts: migrated (0.0362s) ==========================================

== 2 CreateComments: migrating ================================================
-- create_table(:comments)
   -> 0.0367s
== 2 CreateComments: migrated (0.0368s) =======================================

Mongrel not running, can't restart

WHOA.

Not only did it push the commit, it seems to have some post-commit magic to migrate your database and restart mongrel.

Impressions

Overall, this seems pretty slick. Lots of cool stuff going on.

  • Web UI for Rails development if you want it
  • git access for when you want to do local development
  • Effortless deployment
  • Really useful for trying out proof-of-concept apps
  • Free

The only concern I have is pricing. It's free now, but I can only imagine it'll be a pay service at one point or another. I'm told development will be free, but there will be some pricing scheme for making stuff publicly accessible.

May 25, 2008 at 03:59 Permalink Edit Destroy

Managing svn:ignore with impunity

Managing svn:ignore has always annoyed me. After using git for the past few months, it bugs me even more now.

I think I've come up with a simple way of coping. That, or I totally stole it from someone else on the internet, without realizing it. It's hard to keep track of these kinds of things.

The trick

Here's what you do:

  • Create a .svnignore in the top of your project.
  • Make a list of files/wildcards you care to ignore
  • Save it

Now, from the top of your project, you can do:

$ svn propset svn:ignore -F .svnignore  .

If one were to wordify this command, it's be something like "Set the svn:ignore property from the contents of the .svnignore onto the current directory." It can really be any file, but I kinda like this convention.

It is also likely you will want to keep .svnignore under source control.

Now automate it

It's usually best to automate when possible, since even this simplied way of managing ignored files might get tedious. Or maybe someone coming onto the project doesn't have our level svn-fu.

We already have a lib/tasks/svn.rake that has a few tricks in it, so I figured on expanding it. Here's the original:

namespace :svn do
  desc "Adds all files with an svn status flag of '?'"
  task(:add) { system %q(svn status | awk '/\\?/ {print $2}' | xargs svn add) }

  desc "Deletes all files with an svn status flag of '!'"
  task(:delete) { system %q(svn status | awk '/\\!/ {print $2}' | xargs svn delete) }

  desc "Writes the log file to doc/svn_log.txt"
  task(:log) do
    File.delete("#{RAILS_ROOT}/doc/svn_log.txt") if File::exists?("#{RAILS_ROOT}/doc/svn_log.txt")
    File.new("#{RAILS_ROOT}/doc/svn_log.txt", "w+")
    system("svn log >> doc/svn_log.txt")
  end
end
I added a new task that looks like:
desc 'Updates svn:ignore from .svnignore'
task(:update_svn_ignore) do
  system %q(svn propset svn:ignore -F .svnignore .)
end
Now we can tweak `.svnignore` to our hearts' content, and then just run:
$ rake svn:update_svn_ignore
(in /Users/nichoj/Projects/boston_rb)
property 'svn:ignore' set on '.'

Awesome.

May 23, 2008 at 05:09 Permalink Edit Destroy

Better partials with better-partials

The other week, Dan Croak pointed out better-partials to me. Hadn't gotten a chance to play it until now.

The gist of better-partials is that it's a syntactical sugar for render :partial.

Here's a typical haml snippet using a partial:

- form_for(@video) do |form|
  = render :partial => '/videos/form', :locals => { :form => form }

With better-partials, we can refactor it to:

- form_for(@video) do |form|
  = partial 'videos/form', :form => form

This saves a little typing, and makes it a bit more clearer:

  • You just say you want a partial, rather than to render a partial
  • Automatically passes in options to :locals (with a few special exceptions)

There are a few other usages of better-partials, but I haven't gotten to use them yet.

It's not that big of a deal, but it seems like a nice little improvement to me.

May 13, 2008 at 01:53 Permalink Edit Destroy

Boston.rb Hackfest 5/07 Post Mortem

By now, I think it's a running joke that we'd work on the recommendable plugin we started a month or so ago.

Instead, we worked on the Factory stuff. This article captures most of the ideas of using factories for testing and reasons for using them instead of fixtures.

The crew tonight:

Overview

As the article mentions, someone did implement a plugin which captures most of these ideas.

There were a few reasons (and some more which I don't remember) why we didn't like this plugin.

  • Plugin, not a gem, so unusable outside of rails
  • Pollutes the namespace of Object
  • No tests

So, the journey for better Factories begins. The basic ideas/goals we settled on is this:

  • Define your factories in test/test_helper.rb
  • Be able to define a factory for a class with default options
  • Be able to define alternate factories for a class, for example, to create an admin user versus a normal user
  • Flexibility for defining default attributes, ie be able to call other methods
  • Choice of making new instance, persisting a new instance, stubbing an instance, or getting valid attributes
  • Flexible API for actually calling the factories

The API

So we started out with a basic API that Dan Croak had outlined:

Factory.define do |factory|
  factory.add :project do |project|
    project.name {"factoryfu"}
    project.homepage_url {"http://factoryfu.rubyforge.org"}
  end

  factory.add :event, :meeting do |event|
    event.event_type {"Meeting"}
    event.date {Time.now}
    event.description {"an sick meeting"}
  end

  factory.add :event, :hackfest do |event|
    event.event_type {"Hackfest"}
    event.date {Time.now}
    event.description {"a cool hackfest"}
  end
end

As for usage of the factories, we had 3 possibilities:

Factory.create :project, :name => 'ambitious_sphinx'
Factory.create_project :name => 'ambitious_sphinx'
create_project :name => 'ambitious_sphinx'

I'm not going to say much about these, aside from the first being the simplest to implement, the second being the most explicit about what you are doing, and the last is the most concise.

Because it seemed like the simplest, we started implementing the first. I believe that the other two syntaxes can actually be emulated using it.

To simulate the second, as we register factories, we can create the appropriate methods, ie create_event, new_event, valid_event_attributes.

To simulate the third, we can build on the second, and probably do some magic to delegate from TestCase to our Factory.

Implementing it

For simplicity's sake, we started with our work from last week's hackfest on bostonrb.org.

We added the snippet for defining factories into our test_helper.rb, made a new model unit test which used the factory methods, and started a Factory class.

We basically ran the test, watched it blow up horribly, and added some code, until eventually the test passed.

You can see the work we ended up in the hackfest repository under 'boston_rb'

Of particular interest are:

So, I'm just go over a few of the more interesting bits of the implementation:

Factory.define

This is the entrance into defining our factories.

The implementation isn't very interesting. We just call the block on Factory. We were originally making new instances of Factory, and passing that to the block, but we then realized that this made it a little trickier to have factory methods be class-level methods.

Factory.add

This is what you use to define how you the factory behaves.

It takes a block which defines how to create new attributes. If you look at the snippet:

event.event_type {"Hackfest"}
event.date {Time.now}

Notice that event.event_type and event.date are actually taking blocks. This allows us to do Time.now, and have this not actually be invoked until object creation.

Factory.add actually passes your block a magical Hash which captures this attribute/attribute creation data. We'll talk about this has in a little bit.

After we get back from the block, we store in @@model_templates, keyed by the model and kind of the factory.

The Factory methods

We provide three factory methods:

  • valid_attributes: a Hash of valid attributes for the object
  • new: a new unpersisted instance, using valid_attributes
  • create: a persisted instance, using new

You invoke them like:

Factory.valid_attributes :project
Factory.new :project, :name => 'factoryfu', :homepage_url => 'http://somewhere.com'
Factory.create :event, :hackfest, :date => 3.days.ago, :description => 'your looking at it'

The way we parse the arguments is a little tricky. Initially, we only supported the first 2 lines. This meant our method signature looked like:

def self.new(model, attributes = {})

When we went to support the 3rd line, we thought to go like:

def self.new(model, kind = 'default', attributes = {})

By doing so, you break the 2nd line, because kind would get set to our attributes.

activesupport provides a solution: Array#extract_options. Basically, it looks at the Array, and pops the last element off the end if it's a Hash. We use it like:

def self.valid_attributes(model, *args)
  kind, attributes = parse_args(*args)
  # ...
end

protected

def self.parse_args(*args)
  attributes = args.extract_options!
  kind = args.first || 'default'
  [kind, attributes]
end

BlockfulHash

We made a subclass of Hash which lets you define keys like:

event.date {Time.now}
event.description {"This is going to be SICK!"}

Such that it's equivalent to:

event[:date] = {Time.now}
event[:description] = {"This is going to be SICK!"}

Here's the implementation:

class BlockfulHash < Hash
  def method_missing(method, *args)    
    # When Proc.new is given no arguments,
    # it returns a Proc for the block passed to the current method
    # Proc.new... yeah, it's officially a hackfest.
    self[method.to_sym] = Proc.new
  end
end

This BlockfulHash is what gets passed to the block given to Factory.add.

What's next

So, we have something workable here:

  • Uses the syntax we laid out for defining factory
  • Supports the first syntax for using factories (Factory.create :event)

As far as what is left to do:

  • Extract it out of the boston_rb code base
  • More tests
  • Gemify
  • Support Factory.create_event syntax
  • ...
  • PROFIT!!!

Addendum

Some links and other ideas tossed around over the course of the night:

May 07, 2008 at 19:52 Permalink Edit Destroy

Managing RubyGems on Gentoo

For Ruby on Gentoo, it has been asked how one should go about managing RubyGems. You have a few options:

  • Use Gentoo packages exclusively
  • Use gem exclusively
  • Mix and match and pray

Mix and match

Offhand, mixing and matching seems like a bad idea.

Doing so, you might install a gem using portage, but then you could use gem update to update it. Then you update it with portage...

Overall, seems like it could be problematic in the long run.

Gentoo packages exclusively

Being a Gentoo packager, my first take is that using packages is the way to go.

Some pluses:

  • One stop for installing/updating packages, ruby or otherwise
  • For gems with native extensions, can have dependencies on the things the native extensions use

Of course, there are a number of disadvantages.

  • There are tons of gems out there
  • Some update frequently
  • Lot of packages to be keeping up with stable keywords
  • Despite gem ebuilds being pretty straightforward, there is a bit of grunt work involved on to verify dependencies are accurate
  • Lots of gems are unpackaged, because they are usually packaged as requested or as developers need them
  • Despite being able to install multiple gems at once, packages usually aren't slotted that way

Gems exclusively

If you're a Ruby developer, the most natural way would be using RubyGem directly. Some nice things include:

  • Most direct way to have the latest and greatest gems
  • Can have as many versions installed as you want
  • Zero maintenance for Gentoo packagers (sweet!)

Of course, not without their drawbacks:

  • If a gem uses native extensions, it can be tricky to determine what the it's dependencies are
  • Problematic when other (non-ruby) packages need to depend on gems

My current practices

So where does that bring us?

Here are the practices I've taken to using for my Ruby and Rails development and deployment needs.

In development

On my development machine (4 MacBook running Leopard with Gentoo/Prefix), I've using RubyGems directly.

If I'm working on a Rails app, and it needs any gems, I've taken to vendoring everything. This type of thing will be supported out of the box for Rails 2.1.

Of course, this wouldn't work so well for things with native extensions, like hpricot.

In production

I avoid installing any gems in production as much as possible. If an app needs something, it should be vendored, like I just described.

For gems needed by my apps that have native extensions, or are otherwise needed in production (like rake and the mysql gem), again, I'm using RubyGem directly.

May 05, 2008 at 02:57 Permalink Edit Destroy

Boston.rb Hackfest 4/29 Post Mortem

So, we were just kidding about working on the recommendable plugin. We actually worked on the bostonrb.org website some more.

The crew tonight was:

The significant things we worked on were:

  • Projects have descriptions now
  • Events now have a location
  • Reorganized events page
    • left side has all upcoming events
    • center has next event with google map
    • right side has nothing now, but we're thinking to put a calendar type view

Some articles, resources, that came up while hacking away:

The fruits of our labor have been pushed to http://bostonrb.org, and the source can be found at https://svn.thoughtbot.com/hackfest/boston_rb.

April 30, 2008 at 04:04 Permalink Edit Destroy

piston and git for the win

Piston has had support for git (both importing from, and into) for a little over a month.

Before that, I felt like developing a rails app in git was a bit painful. Considering most plugins are kept in subversion, you're only real option is using svn export to install plugins, and check that into git. There was braid, but I really didn't have any luck with it.

But, like I said, it's all good now. It hasn't been officially released (ie as gems), but you can build and install it yourself easily enough.

$ git clone git://github.com/francois/piston.git
$ sudo gem install -y main open4 log4r
$ cd piston
$ sudo rake install_gem

Pretty straight forward to use.

$ cd vendor/plugins
$ piston import git://github.com/technoweenie/restful-authentication.git
INFO main: Guessing the repository type
INFO main: Guessing the working copy type
INFO main: Checking out the repository
INFO main: Copying from Piston::Revision(git://github.com/technoweenie/restful-authentication.git@42083ffa31e0a9792472780854ddd81bcc9b2f61)
INFO main: Checked out "git://github.com/technoweenie/restful-authentication.git" 42083ff to "restful-authentication"

And since it's since it's being developed in a git repository, and on GitHub, it's extremely easy to start contributing.

I mean, if I can contribute something, I'm sure you can pull it off too :)

April 23, 2008 at 01:55 Permalink Edit Destroy

Boston.rb Hackfest 4/15 Post Mortem

Last Tuesday, we had a small, but dedicated showing at the Hackfest:

Going in, we didn't really have a something in mind to work.

We thought to pick up the app we started in merb for organizing katas. But when it came down to it, we figured it'd be a lot quicker if we just did it rails.

We actually wanted something a little more general, kind of a one stop shop for Boston Ruby stuff. Here's what we wanted:

  • Keep track of events (meetings, hackfests, and katas)
  • Projects that have come out of above-mentioned meetings
  • Recent commits from these projects

Tools we used:

You can see the code we ended up with in the hackfest repository.

Or, you can see it in action.

For simplicity's sake, we didn't initially bother with users/permissions/etc, so be gentle with it.

April 22, 2008 at 03:17 Permalink Edit Destroy

Dead easy nginx configuration on Gentoo

Considering the hype about nginx in the Rails community, I figured it was about time to try it out.

Being the slacker that I am, I didn't really want to have to learn how to use it. Fortunately, Ezra gives us a good starting point for a configuration.

Then Chris Wanstrath comes along and creates nginxconfiggenerator to make it even easier, by providing a generator around this configuration.

One kink though. Running on Gentoo and using the latest nginx package. Default template has paths not suitable for this. Bummer.

Being the clever fellow he is, Chris's generator does provide a way around this, by letting you specify your own template to use.

I took a little time to tweak his original template for Gentoo consumption. Behold!

Here's how you'd use it, assuming you've gone through the steps already mentioned in the post.

$ wget http://p.caboo.se/182778.txt -O gentoo_nginx.conf.erb
$ generate_nginx_config config.yml nginx.conf \
      --template gentoo_nginx.conf.erb

You should be all set now.

One last note, you need to set fair: false for your sites. The fair directive doesn't seem to work with the current version we have on Gentoo. I'm not positive, but I think it's related to this post by Ezra

April 18, 2008 at 15:54 Permalink Edit Destroy

Shoulda macros allows you to embrace your inner slacker

So I think I've discovered Shoulda's secret sauce, the reason why you should use it.

Macros.

It's nice and all to have a decent syntax for doing nested contexts, and declaring tests. But the real power comes from having macros that define tests for you.

Here's a fully shoulda-ified model test (from an example on the Shoulda website):

class PostTest < Test::Unit::TestCase
  load_all_fixtures

  should_belong_to :user
  should_have_many :tags, :through => :taggings

  should_require_unique_attributes :title
  should_require_attributes :body, :message => /wtf/
  should_require_attributes :title
  should_only_allow_numeric_values_for :user_id
end

Imagine how much more code you'd have if you were to do this by hand.

It's not magic though. It's pretty easy to write your own. To start with, you can just define them in your test_helper.rb.

Here's the jist of what you do:

  • Define a class method, usually like should_something_something, or maybe something_should_something.
  • In this method, declare a should block. You'd probably want to name this dynamically based on the parameters given to your method.
  • Assert stuff.
  • ...
  • PROFIT!

As an example, here's something I wrote recently:

class Test::Unit::TestCase
  #snip
  def self.should_build_request_path(path)
    should "build request path of #{path}" do
      assert {path == @api_call.build_request_path}
    end
  end
  # snip
end

Then in your tests, you can use it like:

class GetTagsCallTest < Test::Unit::TestCase
  # snip
  should_build_request_path '/v1/tags/get'
  # snip
end

It's pretty straightforward stuff.

If you start to get a lot of these in your test_helper.rb, you can always move them into module, include/extend them. If they are general enough, who knows, maybe release it as a gem.

April 09, 2008 at 10:29 Permalink Edit Destroy

Follow up: Haml no longer be blowing my spot

I just barely posted about Haml blowin my spot. Thanks to the kindness of a reader, Haml is no longer blowin my spot.

The problem

Imagine we have the following snippet:

%div{ :id => "post_#{ post.id }" }
  %h2{:class => 'post_title'}= post.title
  .post_content= post.cached_content

This post.cached_content contains some <pre> tags. As a result, the contents get kind of skewed.

The fix

So, it seems you can do something like:

%div{ :id => "post_#{ post.id }" }
  %h2{:class => 'post_title'}= post.title
  .post_content~ post.cached_content

The key here is that we use ~ instead of = to output stuff. Apparently this is an alias for Haml::Helpers.find_and_preserve. I actually didn't see any documentation about this alias though.

Looking to the future

Nathan Weizenbaum also pointed out that "Incidentally, as of Haml 1.9 pre and textarea will automatically eschew indentation-management." Sick!

April 08, 2008 at 11:20 Permalink Edit Destroy

Haml be blowing my spot

I've heard of the awesome that is Haml before. I've had friends rave about it. But it was only recently that I broke down to actually use it.

So I went forth and migrated my blog's rhtml templates to haml. Hell, I even migrated my resume from straight-up html to Haml.

It's good. Really good. But, I have some regrets.

One of the features of haml is to automatically pretty-print your markup. But, can you think of any elements that would probably be better left un-pretty-printified?

  • <pre>
  • <textarea>

So, if you're wondering why my code snippets are all skewed, you have your answer.

I haven't found a good work around yet. An obvious answer would be using filters, but this doesn't quite work since I have markup stored in my model. There seems to be a :dirty option for Haml which doesn't format things, but I haven't quite found out how to flip that switch.

So the search continues.

April 07, 2008 at 16:49 Permalink Edit Destroy

Meet Merb

Merb has been getting a good amount of attention lately as an alternative to rails. I've been playing with it a little since I've been playing around with blerb.

One thing I've discovered is that some things are a bit underdocumented, both in terms of rdoc and more generalized documentation.

As a result of this, I've decided to start a series of posts 'Meet merb', where I'll talk about some of the bits and pieces I've learned along the way.

February 28, 2008 at 10:44 Permalink Edit Destroy

git: links for 2008-01-05

Git seems to be getting a lot more attention lately. I started using it here and there, and I'm pretty enamored with it so far. Here are some links to get you started.

January 06, 2008 at 14:28 Permalink Edit Destroy

Simple caching of markdown markdown in your model

As I've written about it before, I'm using markdown for this blog. Originally, I would do the translation from markdown to html every time a page is rendered. Seems pretty inefficient, right? Yeah, not so much.

As it turns out, it is really simple to cache this information.

For a little background, here's the schema for my posts:

create_table "posts", :force => true do |t|
  t.string   "title"
  t.text     "content"
end

And the model:

class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content
end

What I want to do is, before a post is saved, to generate html from the markdown and save it to the database. Then, instead of generating it at page render, use the saved html.

ActiveRecord provides several callbacks into its lifecycle. There are a few ways you can add a callback as the RDoc demonstrates. The lifecycle we're really interested is before_save. This is for whenever the post is saved, regardless of it is creating a new one, or updating an existing one.

Before we implement the callback, let's update our schema with a migration.

$ script/generate migration AddCachedContent
class AddCachedContent < ActiveRecord::Migration
  def self.up
    add_column :posts, :cached_content, :text
  end

  def self.down
    remove_column :posts, :cached_content
  end
end

Hmm... one problem: we probably want to cache the content of all existing posts. So, let's add a method that will cache the content.


class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content

  def cache_content
    self.cached_content = BlueCloth.new(self.content).to_html
  end
end

Now we have the means to cache our markdown, we can update our migration to cache the content of each post and save it:

class AddCachedContent < ActiveRecord::Migration
  def self.up
    add_column :posts, :cached_content, :text

    Post.find(:all).each do |post|
      post.cache_content
      post.save!
    end
  end

  def self.down
    remove_column :posts, :cached_content
  end
end

We have everything in place to go ahead and actually add the callback:

class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content

  before_save :cache_content
  def cache_content
    self.cached_content = BlueCloth.new(self.content).to_html
  end
end

We're just about done. All that remains is to update places in the view that generate html from markdown to just use the cached_content instead.

December 18, 2007 at 00:07 Permalink Edit Destroy

The Big Rewrite is Here

When I first started this blog, it was my very first Rails project. I was young. I was naive. I didn't do automated testing.

Fast forward back to September, and I wanted to start adding some new features. I wanted to do it right though, with tests, and all. To my dismay, I found the way I had implemented things weren't so easy to test.

This, along with Rails 2.0 coming along, and some other new libraries/plugins I wanted to learn, led me to starting a complete rewrite.

So, here's a laundry list of what I've been using so far:

I think I'm mostly feature complete, but the most significant missing piece is...

Comments! I mostly need to find a good way of spam filtering. The original version was using simple_captcha, but I've been noticing spam has still been getting through. I'm considering trying akismet, which does have a few Rails plugins.

But yeah, that's it for now. I hope to pump out a few posts about this new stuff I'm using.

December 09, 2007 at 01:24 Permalink Edit Destroy

Cover your butt with rcov

We all know that test coverage is a good thing, right? Right. In Ruby land, the most common way to do this is to use rcov. I'll be covering getting rcov installed, and how to start using it with your Rails project.

The first thing to do is install it. On Gentoo, this is as simple as:

$ sudo emerge rcov

If you aren't fortunate enough to be on Gentoo, or if you just prefer using gems directly, you can just do:

$ sudo gem install rcov

Now you'll want to install the Rails plugin.

I highly recommend using piston to do this. Among other things, it provides a nice compromise between using svn:externals and just checking in plugins into your own repo. Piston is emergable, or as a gem.

If you go this path, you can do

$ cd vendor/plugins
$ piston import http://svn.codahale.com/rails_rcov

Otherwise, use the plugin script with svn:externals...

$ ./script/plugin install -x http://svn.codahale.com/rails_rcov

or without svn:externals...

$ ./script/plugin install http://svn.codahale.com/rails_rcov

We should be in business now.

Try running everything:

$ rake test:test:rcov

Behold, you now have coverage reports! Check them out at coverage/test/index.html

You should be all good now. Take a chance to look around 'rake -T' to see what other tasks are available for you.

How is my coverage looking? Er, well, turns out I have less than 50% coverage for unit and funcitonal tests, and 0% for integration... looks someone has some tests to write...

September 18, 2007 at 02:04 Permalink Edit Destroy

DRY Controllers and Helpers using Forwardable

I don't know about you, but I've found myself wanting the same methods available in both a controller and a view. In my case, I had an 'authorized?' method for determining if a user is logged in.

Initially, I had two implementations, one in ApplicationController, and a different one in ApplicationHelper. I was young, and naive.

After starting some BDD with test/spec and mocha, I ran into problems because they were different. It was past time to be more DRY.

So let's think about this. As it turns out, 'controller' is a method available to your views, so, if you have 'something' in your controller, you should be able to hit it in your view by going 'controller.something'. This would give you something like:

<% if controller.authorized? %>
  Aieeeeeeeeee
<% end %>

I don't really like how that reads though. If only I could get it to read like I originally had when there were two implementations

<% if authorized? %>
  Aieeeeeeeeee
<% end %>

I then remembered reading long ago that you Ruby has built in constructs for doing delegation. But alas, that was ages ago and my memory was blank. After racking my mind, jumping up and down, and praying the seach engine deities, I found it: Forwardable

My intent is to delegate authorized? to the controller. Here's what the ApplicationHelper ends up looking like:

module ApplicationHelper
  extend Forwardable
  def_delegator :controller, :authorized?

  ...
end

Simple and awesome. I can dig that.

September 13, 2007 at 09:13 Permalink Edit Destroy

Authentication and authorization: Remembering were you came from

Most webapps need authorization and authentication of some sort, right?

If you happen to have a copy of Agile Web Development with Rails, then you are fortunate, as it covers adding support for that very thing.

It is a bit simplistic though. Books have to have a finite length, and so some not everything can be covered in super depth.

I followed their recipe for authentication on my blog, and my annoyance was that if you request an authorized action, you get prompted to login, but after logging in, you get sent to a single page you set in code, NOT the resource you were initially requesting.

Fortunately, this is pretty simple to address.

So if you were following things in the book, in ApplicationController, you have something like:

def authorize
  unless User.find_by_id(session[:user_id])
    flash[:notice] = "Please log in"
    redirect_to(:controller => "login" , :action => "login" )
  end
end

So the trick here is to record what was being requested in the session. The URI being requested is available from request's request_uri method. The updated authorize looks like:

def authorize
  unless User.find_by_id(session[:user_id])
    flash[:notice] = "Please log in"
    session[:request_uri] = request.request_uri
    redirect_to(:controller => "login" , :action => "login" )
  end
end

We remember what the URI requested was, and get sent to the login page in UserController. The trick is to do something useful with it in the login action. Initially, login will look like:

def login
  session[:user_id] = nil
  if request.post?
    user = User.authenticate(params[:name], params[:password])
    if user
      session[:user_id] = user.id
      redirect_to(:action => "index" )
    else
      flash[:notice] = "Invalid user/password combination"
    end
  end
end

If the request URI is stashed on the session, we want to redirect it and reset the value, otherwise, just redirect to the default page we had before. Here's the resulting code:

def login
  session[:user_id] = nil
  if request.post?
    user = User.authenticate(params[:name], params[:password])
    if user
      session[:user_id] = user.id
      if session[:request_uri]
        redirect_to(session[:request_uri])
        session[:request_uri] = nil
      else
        redirect_to(:action => "index" )
      end
    else
      flash[:notice] = "Invalid user/password combination"
    end
  end
end

Pretty straightforward, right?

September 12, 2007 at 22:17 Permalink Edit Destroy

RE: dev-ruby/rails: File Collision Free Since September 2007!

I wrote earlier about our ebuilds for Rails being file collision free.

To get around having a blocker exposed to the user, rails had a post dependency on eselect-rails, meaning that eselect-rails would get merged AFTER rails. What I failed to remember, though, was that the eselect module was used during the merge of rails, so it had to be a build-time dependency, meaning it'd be pulled in before rails was merged.

This resulted in bug #191015.

Just changing it to DEPEND, and leaving the blockers in eselect-rails would have made annoying blockers, that would probably be a bit confusing for end-users.

So I did a bit of thinking about what really should block what. Versions less than 1.1.6-r2 are in SLOT=1 and install /usr/bin/rails. Versions 1.2.0, 1.2.1, 1.2.2, and 1.2.3 are in SLOT=2 and install /usr/bin/rails. So why not just make those versions block each other?

For less than 1.1.6-r2, we have:

DEPEND=">=dev-lang/ruby-1.8.2
        -snip-
        !~dev-ruby/rails-1.2.0
        !~dev-ruby/rails-1.2.1
        !=dev-ruby/rails-1.2.3"

And in 1.2.0, 1.2.1, 1.2.2, and 1.2.3, we have:

DEPEND=">=dev-lang/ruby-1.8.5
        -snip-
        !&lt;dev-ruby/rails-1.1.6-r1"

I then removed the blockers in eselect-rails, so now it just depends on eselect.

So here are some use cases, and what happens you try to do emerge -u rails.

If you don't care about these, you can just skip down to the summary.

rails-1.1.6-r1 installed

/usr/bin/rails is from rails-1.1.6-r1

metaverse ~ # emerge -ptvu rails

These are the packages that would be merged, in reverse order:
&