Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails - CanCan HABTM association checks

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.

like image 236
bdx Avatar asked Jan 17 '23 03:01

bdx


1 Answers

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.

like image 73
bdx Avatar answered Jan 25 '23 03:01

bdx