This blog has been archived. Our writing has moved to makandra cards.
The blog of , a Ruby on Rails development team

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!

Growing Rails Applications in Practice
Check out our e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Recent posts

Our address:
makandra GmbH
Werner-von-Siemens-Str. 6
86159 Augsburg
Germany
Contact us:
+49 821 58866 180
info@makandra.de
Commercial register court:
Augsburg Municipal Court
Register number:
HRB 24202
Sales tax identification number:
DE243555898
Chief executive officers:
Henning Koch
Thomas Eisenbarth