Here's one way to model it. Lets say we have a model 'Engagement' which has a start datetime, end datetime and name. An engagement has many users, through another join table called 'user_engagements' (with corresponding UserEngagement model). So we have
User
has_many :user_engagements
has_many :engagements, :through => :user_engagements
Engagement
#fields - starts_at, ends_at (both datetime)
has_many :user_engagements
has_many :users, :through => :user_engagements
UserEngagement
belongs_to :user
belongs_to :engagement
Now we have a nice simple schema. An engagement basically models something that's happening, and user_engagements model users who are booked to do that thing. We have an assumption (not written into the code) that when they are doing something they aren't available to do anything else.
Our next task is to write a method that returns users available within a given time period, ie a new engagement. So, we make an engagement and we want all the users who don't have an engagement which crosses over with our new engagement. I think the simplest way of doing this might be to find all the users who do have a crossing-over engagement and then return all the users who aren't them. If you know what i mean. A more precise way of saying e2 crosses over with e1 is that e2 starts before the end of e1 AND ends after the start of e1.
Let's make this a method of an engagement object since it's totally dependent on an engagement's data.
#in Engagement
def unavailable_user_ids
User.find(:all, :include => [:user_engagements], :select => "users.id", :conditions => ["user_engagements.starts_at < ? and user_engagements.ends_at > ?", self.ends_at, self.starts_at]).collect(&:id)
end
def available_users
User.find(:all, :conditions => ["id not in (?)", self.unavailable_user_ids])
end
I feel like there's a more efficient way to get this in one query but i can't quite put my finger on the sql.