4

I want to model such a relationship between the models User and Event.

Therefore I have started with the following classes:

class User < ActiveRecord::Base
...
end

class Attendance < ActiveRecord::Base
# with columns user_id and event_id
...
end

class Event < ActiveRecord::Base
  has_many :attendances
  has_many :users, :through => :attendances
  ...
end

So far everything is okay: I can assign users and access attendances. But now I want to bring the state into play, such that I can distinguish e.g. between "attending", "unexcused absent", ... users. My first try was:

class Event < ActiveRecord::Base
  has_many :attendances
  has_many :users, :through => :attendances
  has_many :unexcused_absent_users, -> { where :state => 'unexcused' },
                                   :through => :attendances,
                                   :source => :user
  ...
end

(:source has to be specified since otherwise it would search for a belongs to association named 'unexcused_absent_users') The problem here is, that the where-predicate is evaluated on table 'users'.

I am clueless how to solve this 'correctly', without introducing new join tables/models for every state. Especially since every user can be just in one state for every event, I think a solution with one Attendance-model makes sense.

Have you an idea, how to get this right?

Community
  • 1
  • 1
contradictioned
  • 1,253
  • 2
  • 14
  • 26

3 Answers3

4

You can simply narrow the scope to look at the correct table:

  has_many :unexcused_absent_users, -> { where(attendances: {state: 'unexcused'}) },
                               :through => :attendances,
                               :source => :user

Evem better, add this scope to the Attendance model and merge it in:

class Attendance < ActiveRecord::Base
  def self.unexcused
    where state: 'unexcused'
  end
end

class Event < ActiveRecord::Base
  has_many :unexcused_absent_users, -> { merge(Attendance.unexcused) },
                               :through => :attendances,
                               :source => :user      
end
PinnyM
  • 35,165
  • 3
  • 73
  • 81
  • Using merge with a named method in the `Attendance` class is clean. Unfortunately this won't work though. The `Event` class is missing an association named `attendances` and for some reason the association will be readonly. I have no idea why, but when saving through it the `state` set in the scope is lost/ignored/nil. – Mario Zigliotto Jul 02 '13 at 06:09
  • @MarioZigliotto: "The Event class is missing an association named attendances..." - The OP had written in the initial code that `Event` contained the line `has_many :attendances`. Can you explain what you mean? In the solution given by the OP, he refers to `:user_attendances` - perhaps the association name was changed? – PinnyM Jul 02 '13 at 15:25
1

I have found a workaround, but I still think, this is ugly.

class Event < ActiveRecord::Base
  has_many :user_attendances, :class_name => 'Attendance'
  has_many :users, :through => :user_attendances, :source => :user

  has_many :unexcued_absent_user_attendances, -> { where :state => 'unexcused'}, :class_name => 'Attendance'
  has_many :unexcused_absent_users, :through => :unexcued_absent_user_attendances, :source => :user
end

In general: For every state that I want, I have to introduce a new has_many relationship with a scope and on top of that and an according has_many-through relationship.

contradictioned
  • 1,253
  • 2
  • 14
  • 26
  • I ran into a very similar issue and have since logged an issue on the Rails project (https://github.com/rails/rails/issues/10945). I'm not 100% sure it's a real issue or user error on my part but I ended up doing a similar workaround as you. Using this workaround I encountered some strange stuff. For example, I would bet that doing something like this: `Event.create!(title: 'foo', unexcused_absent_user_ids: [1,2,3])` results in the `state` attribute being set to `nil`. If you want to bump my issue on github or have any further feedback on your approach i'd love to hear it. – Mario Zigliotto Jul 02 '13 at 06:15
  • I forgot to mention that I used that example `Event` creation because I think it's very similar to what the `EventsController#create` might receive as a POST from a page where the user is adding an Event and simultaneously flagging the people who had unexcused absences. (i.e. like a bunch of checkboxes with student names) – Mario Zigliotto Jul 02 '13 at 06:17
1

this might work for you?

class Event < ActiveRecord::Base
  has_many :attendances
  has_many :users, :through => :attendances

  def unexcused_absent_users
    User.joins(:attendances)
      .where(:state => 'unexcused')
      .where(:event_id => self.id)
  end
end  

in rails 3+ methods are basically the same as scopes, just less confusing (in my opinion), they are chainable

event = Event.find(xxxx)
event.unexcused_absent_users.where("name LIKE ?", "Smi%")
house9
  • 20,359
  • 8
  • 55
  • 61
  • This has the drawback, that this is not writable. And adding another method `unexcused_abent_users= array` is not that "railsy" in my opinion :\ – contradictioned Jun 30 '13 at 15:48
  • the method `unexcused_abent_users` above returns an `ActiveRelation` object not an array; having the 'stateful' collection be writable doesn't seem like a good idea to me? you could use specific methods to change the state i.e. `event.absent_but_not_excused!(user)` or `user.not_excused_from(event)` - guess it comes down to personal preference – house9 Jun 30 '13 at 16:29