Rails - CanCan HABTM association checks - ruby-on-rails

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.

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.

I think you're still not wording your request as clearly as you could
...the id of the reseller's own Contact
The :reseller of a Contact is another Contact. There is no :contact attribute in Contact. Perhaps you're making things confusing by referring to "reseller role" and "reseller" when you should be referring to user (from the CanCan class) to avoid confusion with the Contact class' :reseller association).
I will assume
reseller role to be able to manage all of the Contacts which have the reseller_id field set to the id of the reseller's own Contact.
to mean
user can manage Contact c where c.reseller_id is the user_id of some Contact in user.contacts
Assuming this is an accurate interpretation:
can :manage, Contact do |c|
user.contacts.where(:user_id => c.reseller_id)
end

Related

MongoDB conditional aggregate query on a HABTM relationship (Mongoid, RoR)?

Rails 4.2.5, Mongoid 5.1.0
I have three models - Mailbox, Communication, and Message.
mailbox.rb
class Mailbox
include Mongoid::Document
belongs_to :user
has_many :communications
end
communication.rb
class Communication
include Mongoid::Document
include Mongoid::Timestamps
include AASM
belongs_to :mailbox
has_and_belongs_to_many :messages, autosave: true
field :read_at, type: DateTime
field :box, type: String
field :touched_at, type: DateTime
field :import_thread_id, type: Integer
scope :inbox, -> { where(:box => 'inbox') }
end
message.rb
class Message
include Mongoid::Document
include Mongoid::Timestamps
attr_accessor :communication_id
has_and_belongs_to_many :communications, autosave: true
belongs_to :from_user, class_name: 'User'
belongs_to :to_user, class_name: 'User'
field :subject, type: String
field :body, type: String
field :sent_at, type: DateTime
end
I'm using the authentication gem devise, which gives access to the current_user helper, which points at the current user logged in.
I have built a query for a controller that satisfied the following conditions:
Get the current_user's mailbox, whose communication's are filtered by the box field, where box == 'inbox'.
It was constructed like this (and is working):
current_user.mailbox.communications.where(:box => 'inbox')
My issue arrises when I try to build upon this query. I wish to chain queries so that I only obtain messages whose last message is not from the current_user. I am aware of the .last method, which returns the most recent record. I have come up with the following query but cannot understand what would need to be adjusted in order to make it work:
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
This query produces the following result:
undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>
I am currently able to accomplish this by doing the following, which I know is very inefficient and want to change immediately:
mb = current_user.mailbox.communications.inbox
comms = mb.reject {|c| c.messages.last.from_user == current_user}
I wish to move this logic from ruby to the actual database query. Thank you in advance to anyone who assists me with this, and please let me know if anymore information is helpful here.
Ok, so what's happening here is kind of messy, and has to do with how smart Mongoid is actually able to be when doing associations.
Specifically how queries are constructed when 'crossing' between two associations.
In the case of your first query:
current_user.mailbox.communications.where(:box => 'inbox')
That's cool with mongoid, because that actually just desugars into really 2 db calls:
Get the current mailbox for the user
Mongoid builds a criteria directly against the communication collection, with a where statement saying: use the mailbox id from item 1, and filter to box = inbox.
Now when we get to your next query,
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
Is when Mongoid starts to be confused.
Here's the main issue: When you use 'where' you are querying the collection you are on. You won't cross associations.
What the where(:messages.last.from_user => {'$ne' => current_user}) is actually doing is not checking the messages association. What Mongoid is actually doing is searching the communication document for a property that would have a JSON path similar to: communication['messages']['last']['from_user'].
Now that you know why, you can get at what you want, but it's going to require a little more sweat than the equivalent ActiveRecord work.
Here's more of the way you can get at what you want:
user_id = current_user.id
communication_ids = current_user.mailbox.communications.where(:box => 'inbox').pluck(:_id)
# We're going to need to work around the fact there is no 'group by' in
# Mongoid, so there's really no way to get the 'last' entry in a set
messages_for_communications = Messages.where(:communications_ids => {"$in" => communications_ids}).pluck(
[:_id, :communications_ids, :from_user_id, :sent_at]
)
# Now that we've got a hash, we need to expand it per-communication,
# And we will throw out communications that don't involve the user
messages_with_communication_ids = messages_for_communications.flat_map do |mesg|
message_set = []
mesg["communications_ids"].each do |c_id|
if communication_ids.include?(c_id)
message_set << ({:id => mesg["_id"],
:communication_id => c_id,
:from_user => mesg["from_user_id"],
:sent_at => mesg["sent_at"]})
end
message_set
end
# Group by communication_id
grouped_messages = messages_with_communication_ids.group_by { |msg| mesg[:communication_id] }
communications_and_message_ids = {}
grouped_messages.each_pair do |k,v|
sorted_messages = v.sort_by { |msg| msg[:sent_at] }
if sorted_messages.last[:from_user] != user_id
communications_and_message_ids[k] = sorted_messages.last[:id]
end
end
# This is now a hash of {:communication_id => :last_message_id}
communications_and_message_ids
I'm not sure my code is 100% (you probably need to check the field names in the documents to make sure I'm searching through the right ones), but I think you get the general pattern.

How to find the maximum number of associated records?

I have Accounts and Users. Account has_many :users and User belongs_to :account.
What I'm trying to find out is the maximum number of users any single account has.
So, it would need to cycle through all the accounts, sum up the users for each account and return the user count for each account or, ideally, just the maximum user count it found in all of them.
Running Rails 4.0.12 and Ruby 2.1.5.
You can loop all the accounts, and perform a count, but it is very inefficient. Use a JOIN and COUNT.
result = Account.select('accounts.id, COUNT(users.id)').joins(:users).group('accounts.id')
The result will be
#<ActiveRecord::Relation [#<Account id: 6>, #<Account id: 4>, #<Account id: 5>, #<Account id: 1>, #<Account id: 3>]>
and each item attributes are
{"id"=>1, "count"=>1}
Therefore if you take each result you have
results.each do |result|
result.id
# => the account id
result.count
# => the count of user per account
end
To have all in one hash
results.inject({}) do |hash, result|
hash.merge!(result.id => result.count)
end
Instead of making a query for each account, I would suggest updating the query to do group by your field, and order by the count:
User.group('account_id').order('count_all DESC').limit(1).count
Sure you can do:
Account.all.each_with_object({}) do |account, hash|
hash[account.name] = account.users.count
end
That will return a hash of all accounts with their user totals as their value.
Something like:
=> { "Account1" => 200, "Account2" => 50 }
To sort it, do something like
results = Account.all.each_with_object({}) do |account, hash|
hash[account.name] = account.users.count
end
sorted = results.sort_by { |acc, ct| ct }.reverse
Also you can use :counter_cache
class User < ActiveRecord::Base
belongs_to :account, counter_cache: count_of_users
end
class Account < ActiveRecord::Base
has_many :users
end
http://guides.rubyonrails.org/association_basics.html
class Account < ActiveRecord::Base
..
def self.max_number_of_users
all.map {|a| a.users.count}.max
end
..
end
If you want to the maximum number of records that the association have (so you know how many columns to plan for in say a spreadsheet), you can do this in one line:
Account.select('accounts.id, COUNT(users.id) as num').left_outer_joins(:users).group('accounts.id').map{|x| x[:num]}.max

how to append database records in to a variable in ruby on rails

I fetched all users from the database based on city name.
Here is my code:
#othertask = User.find(:all, :conditions => { :city => params[:city]})
#othertask.each do |o|
#other_tasks = Micropost.where(:user_id => o.id).all
end
My problem is when loop gets completed, #other_task holds only last record value.
Is it possible to append all ids record in one variable?
You should be using a join for something like this, rather than looping and making N additional queries, one for each user. As you now have it, your code is first getting all users with a given city attribute value, then for each user you are again querying the DB to get a micropost (Micropost.where(:user_id => o.id)). That is extremely inefficient.
You are searching for all microposts which have a user whose city is params[:city], correct? Then there is no need to first find all users, instead query the microposts table directly:
#posts = Micropost.joins(:user).where('users.city' => params[:city])
This will find you all posts whose user has a city attribute which equals params[:city].
p.s. I would strongly recommend reading the Ruby on Rails guide on ActiveRecord associations for more details on how to use associations effectively.
you can do it by following way
#othertask = User.find(:all, :conditions => { :city => params[:city]})
#other_tasks = Array.new
#othertask.each do |o|
#other_tasks << Micropost.where(:user_id => o.id).all
end
Here is the updated code:
#othertask = User.find_all_by_city(params[:city])
#other_tasks = Array.new
#othertask.each do |o|
#other_tasks << Micropost.find_all_by_user_id(o.id)
end
You are only getting the last record because of using '=' operator, instead you need to use '<<' operator in ruby which will append the incoming records in to the array specified.
:)
Try:
User model:
has_many :microposts
Micropost model:
belongs_to :user
Query
#Microposts = Micropost.joins(:user).where('users.city' => params[:city])

Rails, how to do a custom active record join that doesn't use ID

I have two models Users and Friendships in my RoR application
The friendships table contains two integer fields user1 and user2, both these fields represent a relationship.
How would I go about selecting all friends for a given user id that exists in either the user1 or user2 field?
Thanks.
You can use find_by_sql
http://apidock.com/rails/ActiveRecord/Base/find_by_sql/class
Take a look at ActiveRecord's has_many method documentation. I think you may be able to do something like:
class User
has_many :friendships, :finder_sql => lambda { |user| ["SELECT * FROM friendships WHERE user1 = :id OR user2 = :id", :id => user.id] }
def friends
ids = friendships.map(&:user1) + friendships.map(&:user2)
ids.uniq!
ids.delete(id)
User.find(ids)
end
end
class User
def friends
user_ids = Friendship.where(["user1 = :id OR user2 = :id", :id => self.id]).inject([]) {|sum,x| sum << x.user1 && sum << x.user2 }.uniq-[self.id]
User.find(user_ids)
end
end

Rails ActiveRecord - Best way to perform an include?

I have three models:
class Book < ActiveRecord::Base
has_many :collections
has_many :users, :through => :collections
end
class User < ActiveRecord::Base
has_many :collections
has_many :books, :through => :collections
end
class Collection < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
I'm trying to display a list of the books and have a link to either add or remove from the user's collection. I can't quite figure out the best syntax to do this.
For example, if I do the following:
Controller
class BooksController < ApplicationController
def index
#books = Book.all
end
end
View
...
<% if book.users.include?(current_user) %>
...
or obviously the inverse...
...
<% if current_user.books.include?(book) %>
...
Then queries are sent for each book to check on that include? which is wasteful. I was thinking of adding the users or collections to the :include on the Book.all, but I'm not sure this is the best way. Effectively all I need is the book object and just a boolean column of whether or not the current user has the book in their collection, but I'm not sure how to forumlate the query in order to do that.
Thanks in advance for your help.
-Damien
I have created a gem(select_extra_columns) for returning join/calculated/aggregate columns in a ActiveRecord finders. Using this gem, you will be able to get the book details and the flag indicating if the current user has the book in one query.
In your User model register the select_extra_columns feature.
class Book < ActiveRecord::Base
select_extra_columns
has_many :collections
has_many :users, :through => :collections
end
Now in your controller add this line:
#books = Book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:extra_columns => {:belongs_to_user => :boolean},
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
You're going to to want 2 SQL queries, and O(1) based lookups (probably irrelevant, but it's the principle) to check if they have the book.
The initial calls.
#books = Book.all
#user = User.find(params[:id], :include => :collections)
Next, you're going to want to write the books the user has into a hash for constant time lookup (if people won't ever have many books, just doing an array.include? is fine).
#user_has_books = Hash.new
#user.collections.each{|c|#user_has_books[c.book_id] = true}
And on the display end:
#books.each do |book|
has_book = #user_has_books.has_key?(book.id)
end
I'd err away from caching the book_ids on the user object, simply because going this route can have some funny and unexpected consequences if you ever start serializing your user objects for whatever reason (i.e. memcached or a queue).
Edit: Loading intermediary collection instead of double loading books.
Essentially you need to make one call to get the book information and the Boolean flag indicating if the current user has the book. ActiveRecord finders doesn't allow you to return the join results from another table. We work around this problem by doing a trick.
In your Book model add this method.
def self.extended_book
self.columns # load the column definition
#extended_user ||= self.clone.tap do |klass|
klass.columns << (klass.columns_hash["belongs_to_user"] =
ActiveRecord::ConnectionAdapters::Column.new(
"belongs_to_user", false, "boolean"))
end # add a dummy column to the cloned class
end
In your controller use the following code:
#books = Book.extended_book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
Explanation:
In the extended_book method you are creating a copy of Book class and adding a dummy column belongs_to_user to the hash. During the query extra join column is not rejected as it exists in the columns_hash. You should use the extended_book only for querying.
If you use it for CRUD operations DB will throw error.
I would first create an instance method in the User model that 'caches' the all the Book ID's in his collection:
def book_ids
#book_ids ||= self.books.all(:select => "id").map(&:id)
end
This will only execute the SQL query once per controller request. Then create another instance method on the User model that takes a book_id as a parameter and checks to see if its included in his book collection.
def has_book?(book_id)
book_ids.include?(book_id)
end
Then while you iterate through the books:
<% if current_user.has_book?(book.id) %>
Only 2 SQL queries for that controller request :)
Use exists? on the association as it is direct SQL call. The association array is NOT loaded to perform these checks.
books.users.exists?(current_user)
This is the SQL executed by Rails.
SELECT `users`.id FROM `users`
INNER JOIN `collections` ON `users`.id = `collections`.user_id
WHERE (`users`.`id` = 2) AND ((`collections`.book_id = 1)) LIMIT 1
In the above SQL current_user id = 2 and book id is 1
current_user.books.exists?(book)
This is the SQL executed by Rails.
SELECT `books`.id FROM `books`
INNER JOIN `collections` ON `books`.id = `collections`.book_id
WHERE (`books`.`id` = 3) AND ((`collections`.user_id = 4)) LIMIT 1
In the above SQL current_user id = 4 and book id is 3
For more details, refer to the documentation of the exists? method in a :has_many association.
Edit: I have included additional information to validate my answer.

Resources