views:

140

answers:

2

I have a typical many-to-many relationship using has_many => :through, as detailed below.

class member
  has_many member_roles
  has_many roles, :through => :member_roles
end

class role
  has_many member_roles
  has_man members, :through => :member_roles
end

class member_role
  belongs_to :member
  belongs_to :role
  # has following fields: member_id, role_id, scope, sport_id
end

What I'm trying to do here is allow members to be assigned roles. Each member role has a scope, which by default is set to "All" but if desired can be set to "Sport". If the scope is set to sport then we also capture the sport_id, which allows us to restrict assess on that role to a particular sport (ie, can only manage the teams of that sport, rather than the teams of every sport). Sounds simple enough.

I have setup my update_member_roles action something like this:

def update

  # Assume we passing a param like: params[:member][:roles]
  # as an array of hashes consisting of :role_id and if set, :sport_id

  roles = (params[:member] ||= {}).delete "roles"
  @member.roles = Role.find_all_by_id(roles.map{|r| r["role_id"]})
  if @member.update_attributes params[:member]
    flash[:notice] = "Roles successfully updated."
    redirect_to member_path(@member)
  else
    render :action => "edit"
  end
end

The above works nice enough, it sets the appropriate member_roles very nicely... but as I'm working on the Role model and not the MemberRole model I'm kind of stuck as to how I can access the joining model to set the :scope and :sport_id.

Any pointers here would be much appreciated.

A: 

Sounds like you should look into using nested attributes:

Alex Reisner
Hi, thanks for your answer, although to be honest I'm not sure that really helps. I've watched those screencasts but the situation here is slightly different.
aaronrussell
+1  A: 

You should use the member_roles association instead of roles association.

  def update
    # The role hash should contain :role_id, :scope and if set, :sport_id
    roles = ((params[:member] ||= {}).delete "roles") || []
    MemberRole.transaction do 

      # Next line will associate the :sport_id and :scope to member_roles
      # EDIT: Changed the code to flush old roles.
      @member.member_roles = roles.collect{|r| MemberRole.new(r)}

      # Next line will save the member attributes and member_roles in one TX
      if @member.update_attributes params[:member]
        # some code
      else
        # some code
      end
    end
  end

Make sure you enclose everything in one transaction.

Other possibility is to use accepts_nested_attributes_for on member_roles association.

KandadaBoggu
Kind of. The only thing is this isn't deleting old roles if I change the selected roles. I suppose I could just put @member.member_roles.destroy_all at the start of the action but that feels a bit like I'm wielding a sledgehammer to crack a nut.
aaronrussell
You can delete the old values and assign new values by changing the code as follows:`@member.member_roles = roles.collect{|r| MemberRole.new(r)}`
KandadaBoggu
I have edited my answer to address your issue. Since you are storing additional information in the `MemberRole` model(apart from `member_id` and `role_id`), this is a reasonable approach.
KandadaBoggu
Thanks. I'd cobbled together something similar last night, although you solution is a lot more succinct. Thanks a lot.
aaronrussell
One more question if you don't mind, what does wrapping this in `MemberRole.transaction do` actually achieve?
aaronrussell
The line `@member.member_roles = roles.collect{|r| MemberRole.new(r)}` deletes the old rows and creates new rows in the `member_roles` table. If for some reason the next statement `@member.update_attributes` fails you will have phantom rows in `member_roles` table. By enclosing the statements in a transaction you are ensuring that the changes to `member_roles` table are rolled back upon any error during update to `members` table.
KandadaBoggu