0

The Ruby on Rails app I am working on allows users to create and share agendas with other users.

In addition, we must be able to:

  • Display a list of agendas for each user, on his profile
  • Display a list of users associated with an agenda, on the agenda's page
  • When sharing an agenda with another user, define a role for this user, and display the role of this user on the list mentioned right above

I was going to go with a has_and_belongs_to_many association between the user and the agenda models, like that:

class User < ActiveRecord::Base
  has_and_belongs_to_many :agendas
end

class Agenda < ActiveRecord::Base
  has_and_belongs_to_many :users
end

But then I wondered whether this would let me get and display the @user.agenda.user.role list of roles on the given agenda page of a given user.

And I thought I should probably go with a has_many :through association instead, such as:

class User < ActiveRecord::Base
  has_many :roles
  has_many :agendas, through: :roles
end

class Role < ActiveRecord::Base
  belongs_to :user
  belongs_to :agenda
end

class Agenda < ActiveRecord::Base
  has_many :roles
  has_many :users, through: :roles
end

And although I was pretty comfortable about the idea of a user having several roles (one for each agenda), I am not sure about the idea of an agenda having several roles (one for each user?).

Finally, to add to the confusion, I read about the polymorphic association and thought it could also be a viable solution, if done this way for instance:

class Role < ActiveRecord::Base
  belongs_to :definition, polymorphic: true
end

class User < ActiveRecord::Base
  has_many :roles, as: :definition
end

class Agenda < ActiveRecord::Base
  has_many :roles, as: :definition
end

Does any of the above solutions sound right for the situation?

UPDATE: Doing some research, I stumbled upon this article (from 2012) explaining that has_many :through was a "smarter" choice than has_and_belongs_to_many. In my case, I am still not sure about the fact that an agenda would have many roles.

UPDATE 2: As suggested in the comments by @engineersmnkyn, a way of solving this would be to go with two join tables. I tried to implement the following code:

class User < ActiveRecord::Base
  has_many :agendas, through: :jointable
end

class Agenda < ActiveRecord::Base

end

class Role < ActiveRecord::Base

end

class Jointable < ActiveRecord::Base
  belongs_to :user
  belongs_to :agenda
  has_many :agendaroles through :jointable2
end

class Jointable2 < ActiveRecord::Base
  belongs_to :roles
  belongs_to :useragenda
end

I am not sure about the syntax though. Am I on the right track? And how should I define the Agenda and the Role models?

UPDATE 3: What if I went with something like:

class User < ActiveRecord::Base
      has_many :roles
      has_many :agendas, through: :roles
    end

    class Role < ActiveRecord::Base
      belongs_to :user
      belongs_to :agenda
    end

    class Agenda < ActiveRecord::Base
      has_many :roles
      has_many :users, through: :roles
    end

and then, in the migration file, go with something like:

class CreateRoles < ActiveRecord::Migration
  def change
    create_table :roles do |t|
      t.belongs_to :user, index: true 
      t.belongs_to :agenda, index: true
      t.string :privilege
      t.timestamps
    end
  end
end

Would I be able to call @user.agenda.privilege to get the privilege ("role" of creator, editor or viewer) of a given user for a given agenda?

Conversely, would I be able to call @agenda.user.privilege ?

Thibaud Clement
  • 6,607
  • 10
  • 50
  • 103
  • 1
    Why not a set up a user to have many agendas through a join table that belongs to user and agenda and has many agenda roles through a join table that belongs to user agenda and roles . I'd write it up for you but I'm not at a computer right now. – engineersmnky Jun 19 '15 at 01:28
  • Thanks @engineersmnky this makes a lot of sense, since it fixes the problem of agendas not having many roles. If I understood correctly what you suggested, the code would look something like: https://repl.it/tNS I am not sure about the syntax though. – Thibaud Clement Jun 19 '15 at 01:42

1 Answers1

1

Okay I will preface by saying I have not tested this but I think one of these 2 choices should work well for you.

Also if these join tables will never need functionality besides a relationship then has_and_belongs_to_many would be fine and more concise.

Basic Rails rule of thumb:

If you need to work with the relationship model as its own entity, use has_many :through. Use has_and_belongs_to_many when working with legacy schemas or when you never work directly with the relationship itself.

First using your example (http://repl.it/tNS):

class User < ActiveRecord::Base
  has_many :user_agendas
  has_many :agendas, through: :user_agendas
  has_many :user_agenda_roles, through: :user_agendas
  has_many :roles, through: :user_agenda_roles

  def agenda_roles(agenda)
    roles.where(user_agenda_roles:{agenda:agenda})
  end
end

class Agenda < ActiveRecord::Base
    has_many :user_agendas
    has_many :users, through: :user_agendas
    has_many :user_agenda_roles, through: :user_agendas
    has_many :roles, through: :user_agenda_roles

    def user_roles(user)
      roles.where(user_agenda_roles:{user: user})
    end
end

class Role < ActiveRecord::Base
  has_many :user_agenda_roles
end

class UserAgenda < ActiveRecord::Base
  belongs_to :user
  belongs_to :agenda
  has_many   :user_agenda_roles
  has_many   :roles, through: :user_agenda_roles 
end

class UserAgendaRoles < ActiveRecord::Base
  belongs_to :role
  belongs_to :user_agenda
end

This uses a join table to hold the relationship of User <=> Agenda and then a table to join UserAgenda => Role.

The Second Option is to use a join table to hold the relationship of User <=> Agenda and another join table to handle the relationship of User <=> Agenda <=> Role. This option will take a bit more set up from a CRUD standpoint for things like validating if the user is a user for that Agenda but allows a little flexibility.

class User < ActiveRecord::Base
  has_many :user_agendas
  has_many :agendas, through: :user_agendas
  has_many :user_agenda_roles
  has_many :roles, through: :user_agenda_roles

  def agenda_roles(agenda)
      roles.where(user_agenda_roles:{agenda: agenda})
  end

end

class Agenda < ActiveRecord::Base
    has_many :user_agendas
    has_many :users, through: :user_agendas
    has_many :user_agenda_roles
    has_many :roles, through: :user_agenda_roles

    def user_roles(user)
        roles.where(user_agenda_roles:{user: user})
    end
end

class Role < ActiveRecord::Base
  has_many :user_agenda_roles
end

class UserAgenda < ActiveRecord::Base
  belongs_to :user
  belongs_to :agenda
end

class UserAgendaRoles < ActiveRecord::Base
  belongs_to :role
  belongs_to :user
  belongs_to :agenda
end

I know this is a long answer but I wanted to show you more than 1 way to solve the problem in this case. Hope it helps

engineersmnky
  • 25,495
  • 2
  • 36
  • 52
  • Thank you very much for your time and help @engineersmnky. I am going to give both your solutions a try. Agree with you about the has_and_belongs_to_many limitations that prevent us from working directly with the relationship. One question though: what do you think about the solution I post in UPDATE 3? – Thibaud Clement Jun 19 '15 at 13:57
  • 1
    @ThibaudClement The issue I see there is that you would have to set up the same Roles multiple times for every new agenda or user. I was under the assumption a role would be something like owner, editor, etc. in which case you would really only want 1 reference to each role and it's applicable permissions rather than having to set them, up every time. If I misunderstand the "role" Roles play in this set up then your solution may work for you as it is a valid relationship. – engineersmnky Jun 19 '15 at 14:16
  • Again, Thank you very much @engineersmnky. Sorry for not being accurate in my question. Let me make clear things up. There would be three different possible roles: 1. Owner (the person building the agenda in the first place), 2. Editor (a person with whom the agenda has been shared and who has the privilege to edit it) and 3. Viewer (a person with whom the agenda has been shared and who can only see it but not edit it). As this is a collaborative app, we could have several Editors and Viewers (and even owners) for each agenda. Does this make more sense? – Thibaud Clement Jun 19 '15 at 14:30
  • 1
    @ThibaudClement in that case your third update would be fine although I would possibly create this as a polymorph then in the event you want to create additional roles. I don't think that Role should handle all this logic based on a string in the table. That seems like it will create a lot of conditional logic in the model. – engineersmnky Jun 19 '15 at 15:19
  • Thanks a lot @engineersmnky . Sorry if this is a dumb question, but why do you say the has_many :through solution would create a lot of conditional logic in the model? Indeed, we will define permissions/privileges (edit, approve, view) associated with each role (owner, editor, viewer) but shouldn't that take place in the controllers? For instance, couldn't I test what what a user's role is in the controller and then display in the corresponding view the links to perform the permitted actions? Again, sorry if this is dumb. – Thibaud Clement Jun 19 '15 at 16:07
  • 1
    @ThibaudClement you can make multiple views dependent upon "roles" but this is only a visual constraint not a functional one. I think that separate models for Owner Viewer Editor would make more sense. But my concern with conditionals is things like `@user.can_edit?(@agenda)` – engineersmnky Jun 19 '15 at 16:43
  • 1
    then the method would have to be something like `def can_edit?(agenda); roles.where(agenda: agenda).any?(&:allow_edits?);end` and the Role would need a `def allow_edits?; ['Owner','Editor'].includes?(self.privilege);end` and you will end up with a lot more of these using `includes?` or `if...else` or `case` statements to detemine privileges based on a String. If you make these models and make the relation ship a polymorph then it would be more like `class Owner;def allow_edits?;true;end;end` – engineersmnky Jun 19 '15 at 16:46
  • Thanks @engineersmnky . If Owner, Editor and Viewer become separate models, should they be subclass of the User class? – Thibaud Clement Jun 19 '15 at 16:55
  • Also, I stumbled upon this article — http://goo.gl/H9iZdj — and this thread — http://goo.gl/c0t2ZI — and it seems the Cancancan gem could handle permissions based on a Role model. This would then require a User model, an Agenda model, an Ability model and a Role model, the latter being defined by an id (first column) and the combination of a User.id (second column), an Agenda.id (third column) and an Ability (fourth column)? Or does this leads to the same issue of too much conditional logic in the model? – Thibaud Clement Jun 19 '15 at 17:00