Using dynamic has_many conditions to save nested forms within a scope

Named scopes are a convenient way to filter record lists. When we want all future bookings of a given event, we can define a future scope for our booking:

class Event
  has_many :bookings
end

class Booking
  named_scope :future, lambda {{ :conditions => ['date >= ?', Date.today] }}
end

It gets tricky when you want a form to edit all future bookings of an event using nested forms. Nested forms weren't designed with scopes in mind, they only play nice with has_many associations.

Here is a little hack to make nested forms play along. Write your has_many association like this:

class Event
  attr_accessor :bookings_filter_sql
  has_many :bookings, :conditions => '#{@bookings_filter_sql}'
end

The single quotes around the conditions are not a typo. It's to prevent Ruby from interpolating the string at compile time.

You can now set the bookings_filter_sql attribute of an event to an SQL condition. All bookings of that event instance will then be scoped to that condition:

class EventController
  def update
    @event = Event.find(params[:id])
    @event.bookings_filter_sql = "date >= '#{Date.today.to_s(:db)}'"
    @event.attributes = params[:event]
    @event.save!
  end
end

I'd like to recommend two improvements to this hack. First, you need to be super careful to not allow SQL injections when bookings_filter_sql gets directly piped into has_many. Better make that attribute a getter that sanitizes the SQL:

class Event
  has_many :bookings, :conditions => '#{bookings_filter_sql}'
  attr_writer :bookings_filter
  def bookings_filter_sql
    self.class.send(:sanitize_sql_for_conditions, @bookings_filter)
  end
end

You can now set the filter with all the sugar you're used to:

@event.bookings_filter = ['date >= ?', Date.today.to_s(:db)]

Secondly, the bookings association as written above only works when a filter is set. So let's give it a alternate condition that is always true (like '1=1') for that case:

class Event
  has_many :bookings, :conditions => '#{bookings_filter_sql}'
  attr_writer :bookings_filter
  def bookings_filter_sql
    self.class.send(:sanitize_sql_for_conditions, @bookings_filter || '1=1')
  end
end

Was this post helpful to you? Then let us know!

You can follow any response to this post through the Atom feed.

Avatar

Sun, 21 Mar 2010 20:27:00 GMT

by henning

Tags:

Leave a comment