Rails - CanCan HABTM association checks
cancan, ruby-on-rails
Solution
Edited 2015-03-26
Having noticed that this question/answer was getting a bit of attention I thought I should point out a better method I've found since.
When creating has_one/has_many associations, rails creates `foreign_model_id`/`foreign_model_ids` methods respectively. These methods return an integer or array of integers respectively.
That means instead of the answer below, the entry in the `ability.rb` file can be simplified without having to use that ugly logic to create my own array of objects and iterate through them to:
`can :manage, Contact, id: (user.contact_ids + user.reseller_client_ids)`
Previous answer kept for posterity
Fixed by using this in my Ability.rb file:
# Manage all contacts associated to this reseller
reseller_contacts = user.contacts
managed_contacts = []
reseller_contacts.each do |i|
i.reseller_clients.each do |rc|
managed_contacts << rc.id
end
managed_contacts << i.id
end
can :manage, Contact, :id => managed_contacts
Deefour, thanks for your help along the way, don't think I'd have got there without your comments.
Problem
I have models set up as follows (self-association in contacts because information I wanted to store for resellers mirrored all fields in that table, seemed in keeping with DRY to use the already existing data structures): ``` class Contact < ActiveRecord::Base attr_accessible :reseller_id has_and_belongs_to_many :users has_many :reseller_clients, :class_name => "Contact", :foreign_key => "reseller_id" belongs_to :reseller, :class_name => "Contact" end class User < ActiveRecord::Base attr:accessible :name has_and_belongs_to_many :contacts end ``` With cancan, I want to have a reseller login that is able to manage their own contact. The mapping between users and resellers is HABTM, so this can be achieved by doing `can :manage Contact, :users => {:id => user.id}` as below. I also want the reseller login to be able to manage all Contact's which match the set described by managed_accounts in the following logic: ``` reseller_contacts = user.contacts managed_accounts = [] reseller_contacts.each do |i| managed_accounts << i.reseller_clients end managed_accounts.flatten! ``` My current Ability class has: ``` class Ability include CanCan::Ability def initialize(user) if user.role? :reseller # Allow resellers to manage their own Contact can :manage, Contact, :users => {:id => user.id} # This works correctly at present # Allow resellers to manage their client Contacts can :manage, Contact, :reseller => {:users => {:id => user.id}} #This doesn't work end end end ``` The error I receive with it as it is, is as follows: ``` Mysql2::Error: Unknown column 'contacts.users' in 'where clause': SELECT `contacts`.* FROM `contacts` INNER JOIN `contacts` `resellers_contacts` ON `resellers_contacts`.`id` = `contacts`.`reseller_id` INNER JOIN `contacts_users` ON `contacts_users`.`contact_id` = `resellers_contacts`.`id` INNER JOIN `users` ON `users`.`id` = `contacts_users`.`user_id` INNER JOIN `contacts_users` `users_contacts_join` ON `users_contacts_join`.`contact_id` = `contacts`.`id` INNER JOIN `users` `users_contacts` ON `users_contacts`.`id` = `users_contacts_join`.`user_id` WHERE ((`contacts`.`users` = '---\n:id: 6\n') OR (`users`.`id` = 6)) ``` My understanding of cancan is that it checks on a per contact basis what is and isn't permitted. If I could do what I wanted in a block, it would appear as follows (Covers both the resellers own contact and all contacts which are clients of the reseller): ``` can :manage, Contact do |contact| user.contacts.exists?(contact.reseller_id) || user.contacts.exists?(contact.id) end ``` I can't use a block for this however, as when trying to use `@contacts = Contact.accessible_by(current_ability)` in my index action on the controller, I get: ``` The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for :index Contact(id: integer, first_name: string, last_name: string, postal_addr_line_1: string, postal_addr_line_2: string, postal_addr_line_3: string, postal_addr_city: string, postal_addr_post_code: string, postal_addr_country: string, billing_addr_line_1: string, billing_addr_line_2: string, billing_addr_line_3: string, billing_addr_city: string, billing_addr_post_code: string, billing_addr_country: string, contact_email: string, company_name: string, phone_home: string, phone_work: string, phone_mobile: string, split_bills: boolean, created_at: datetime, updated_at: datetime, reseller_id: integer) ``` Edit: ALMOST solved, now I just have a problem of combining abilities: I changed the working part of my Ability model to read as: ``` reseller_contacts = user.contacts managed_accounts = [] reseller_contacts.each do |i| i.reseller_clients.each do |rc| managed_accounts << rc.id end end can :manage, Contact, :id => managed_accounts can :manage, Contact, :users => {:id => user.id} can :create, Contact ``` Now the only problem is that the first `can :manage` line gets overwritten by the second one. I was under the impression that they should be additive, not replacing. More research required, but I think this question itself is fixed by the above. Now I need to work out how to make both `can :manage` lines apply.