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)
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With