So I'm trying filter when a User's role name matches the Region's name and I'm getting stuck on the Arel join between the three tables.
User has the name, role_id, email, etc. User has_many roles. Role table has the id, and name. Region has name, location. Region does not belong to User.
Example:
John Smith id: 20, role_id: 15
Role id:15, name: Pacific Northwest
Region id: 5, name: Pacific Northwest
So my thought would be user join Region where Region name matches
What I have so far:
scope :for_user, lambda { |user|
if user.super_admin?
self
else
joins(:region).where(
Region.arel_table[:name].matches(Role.arel_table[:id].eq(User.arel_table[:role_id]))
)
end
}
def viewable_by?(user)
return user.super_admin? || user&.role.name == region.name
end
I'm referencing the for_user and viewable_by in an Event Controller
def index
#events = search_event.paginate(page: params[:page])
end
def search_event
Event.order(sortable_query)
.for_user(current_user)
.includes(:region)
.advanced_search(
params[:search],
params[:region_id]
)
end
def load_event
#event = Event.find_by!(id: params[:id])
raise DeniedError unless #event.viewable_by?(current_user)
end
I thought it might be a matches issue so I also tried on instead. Same issue comes up: no implicit conversion of Symbol into String.
The idea is a user has a role assigned to them and can only view those Events that have a name that matches that role.
So if John Smith is logged in he'd only see those Events of Pacific Northwest.
I may not have fully grokked your question, but it seems like you ought to be able to do something like:
Event < ApplicationRecord
belongs_to :region
class << self
def for_user(user)
return self if user.super_admin?
joins(:region).where(region: {id: Region.where(name: user.role.name)})
end
end
end
That should return all Events where the event.region_id holds the id of any Region that has a name that matches the name in the user's role.
You can learn more in Specifying Conditions on the Joined Tables from the docs.
I see that you have an approach that works using arel_table. I'll leave this here as a non-arel_table, non-lambda, non-scope approach.
Also, I'll reiterate from my comment that the docs state:
Using a class method is the preferred way to accept arguments for scopes.
On a lark I tried, and it worked:
joins(:region).where(
Region.arel_table[:name].matches("%#{user&.role.name}%")
)
Related
I am looking for a way of designing a Balance model between two users in Ruby on Rails (but could be more general). Two users Alice and Bob would start with a balance of value zero with regards to the other one. Now, if Alice gets a +4, I want Bob to have -4. Here's what I came up with so far:
A balance with three fields : user_one, user_two, and one_to_two. That is if I want the balance for user_one to user_two I just take one_to_two, and if I want from two_to_one I take -one_to_two (just a method in the balance model). The problem is that I could not simply use 'has_many balances through' from the User model, since the balance would need to know if it's user_one or user_two who is calling (I would have to pass the User)
A balance with three fields again, but completely asymmetric: if I create a balance for Alice to Bob, I would have to create another instance of balance from Bob to Alice. This would double the storage needed for each 'balance' in the database, and would require updating the bob_to_alice everytime I update alice_to_bob.
Can anyone come up with something better, or explain the reason for choosing one of the above? Let me know if I can make anything clearer.
So, I think I found something pretty neat. It uses the fact that user alice is always the one with the smallest id (via :ensure_alice_before_bob). The rest should be clear.
The User model just has two extra "has_many :balances_as_alice, has_many :balances_as_bob". It could work with only one (for instance :balances_as_alice), but with those two present + dependent: :destroy you make sure to avoid orphan balances.
# == Schema Information
#
# Table name: balances
#
# id :integer not null, primary key
# alice_id :integer not null
# bob_id :integer not null
# alice_value :float default("0.0")
# created_at :datetime not null
# updated_at :datetime not null
#
class Balance < ActiveRecord::Base
belongs_to :alice, class_name: 'User'
belongs_to :bob, class_name: 'User'
before_validation :ensure_alice_before_bob
validates :alice_id, presence: true
validates :bob_id, presence: true, uniqueness: { scope: :alice_id}
validate :different_users
def value
#for_bob ? -alice_value : alice_value
end
def value=(new_val)
#for_bob? self.alice_value = -new_val : self.alice_value = new_val
end
def Balance.get_for alice, bob
user = 'alice'
alice, bob, user = bob, alice, 'bob' if alice.id > bob.id
balance= alice.balances_as_alice.find_by(bob: bob) ||
Balance.new(bob: bob, alice: alice, alice_value: 0.0)
balance.send("for_#{user}")
end
def for_bob
#for_bob = true; self
end
def for_alice
#for_bob = false; self
end
private
def ensure_alice_before_bob
return if self.alice_id.nil? || self.bob_id.nil?
unless self.alice_id < self.bob_id
self.bob_id, self.alice_id = self.alice_id, self.bob_id
end
end
def different_users
if self.alice_id == self.bob_id
errors.add(:bob_id, "Cannot be the same user")
end
end
end
I am just adding search to my project to be able to find people by name. but on my db i have first_name and last_name but if someone searches for a full name like Joe Doe no result matches
#model
class Person < ActiveRecord::Base
def full_name
(self.first_name + ' ' + self.last_name).titleize
end
end
#controller
class PeoplesController < ApplicationController
expose(:peoples){
if params[:search]
People.where(
"first_name ILIKE ?
OR last_name ILIKE ?
", params[:search], params[:search]
)
else
People.all
end
}
end
Again if someone searches a first_name it comes back with results, last_name it comes back with results but not for a full name
and if i try to add full_name to the query i get column "full_name" does not exist
Thanks for the help
A virtual field is not on database-level. You can't do database-based search without explaining what that virtual field is to the database.
The definition of your field is essentially a list of columns it consists of. Since you are using PostgreSQL, you could leverage its full-text searching capabilities by using pg_search. It's well able to search by multiple columns at once.
In fact, in the docs there is an example of how to do it that almost exactly matches your case. I literally just copy-pasted it here. Go figure.
# Model
class Person < ActiveRecord::Base
include PgSearch
pg_search_scope :search_by_full_name, :against => [:first_name, :last_name]
end
# Example code for the Rails console
person_1 = Person.create!(:first_name => "Grant", :last_name => "Hill")
person_2 = Person.create!(:first_name => "Hugh", :last_name => "Grant")
Person.search_by_full_name("Grant") # => [person_1, person_2]
Person.search_by_full_name("Grant Hill") # => [person_1]
Is that sort of thing worth an extra dependency, is up to you. If you find yourself in situation of constructing many complicated searches, this might help.
Again if someone searches a first_name it comes back with results, last_name it comes back with results but not for a full name
That's expected because none of the fields contain the whole name, just the parts, hence they can never match. A simple way out of this is to just split the seatch term by space and check whether you got two or more items.
# app/queries/people_search_query.rb
class PeopleSearchQuery
attr_reader :relation
def initialize(relation = Person.all)
#relation = relation
end
def search(params)
if params[:search]
where_str = "first_name ILIKE ? OR last_name ILIKE ?"
split = params[:search].split(" ", 2)
if split.size > 1
relation.where(where_str, *split)
else
relation.where(where_str, *(split * 2))
end
else
relation
end
end
end
class PeoplesController < ApplicationController
expose(:peoples) { PeopleSearchQuery.new.search(params) }
end
Basic association setup (note, Customer is an extension of a Person model):
Customer has_many :orders
Order belongs_to :customer
Inside of Customer.rb, I have the following class method:
# SCOPE
def self.ordered_in_90_days
joins(:orders).where('orders.created_at > ?', 90.days.ago)
end
In my test, I have the following code which creates a new Order (automatically creating a customer thanks for FactoryGirl), then uses the Customer model's self method defined above:
it "finds customers who have ordered within the last 90 days" do
#order = FactoryGirl.create(:order, created_at: 50.days.ago)
#customer = #order.customer
Customer.count.should == 1 #passes
Order.count.should == 1 #passes
puts Customer.all.to_yaml #for debugging, see below
puts Order.all.to_yaml #for debugging, see below
Customer.ordered_in_90_days.should == [#customer] #fails! returns: []
end
Both the customer and order are being created, but nothing is returning in the method call (empty array). What am I missing?
Here is some additional information regarding the factories:
FactoryGirl.define do
factory :customer do
first_name "Wes"
last_name "Foster"
type "Customer"
end
factory :order do
customer
end
end
And here is the debugging output for Customer and Order (remember, Customer is an extension of Person, so that's why you see person_id instead of customer_id):
---
- !ruby/object:Customer
attributes:
id: 1
first_name: Wes
last_name: Foster
type: Customer
created_at: 2013-09-16 21:54:26.162851000 Z
updated_at: 2013-09-16 21:54:26.162851000 Z
middle_name:
---
- !ruby/object:Order
attributes:
id: 1
person_id:
created_at: 2013-07-28 21:54:26.135748000 Z
updated_at: 2013-09-16 21:54:26.192877000 Z
(Customer
The debug output indicates the problem, indeed! Take a look at the Order inspect: you have person_id blank.
First, even if a Customer is a subclass/extension of Person, the Order belongs_to :customer tells ActiveRecord to look for customer_id, not person_id. Are you indicating that the association should be configured in a non-default way on the Order model?
Otherwise, I think you might be mishandling the aliased association reference in the Order factory. I've not used factory_girl association alias references in my project—I try to keep associations out of my factories—but I would verify your methodology with the factory_girl documentation: Association Aliases.
I would, personally, try this in your test example:
it "finds customers who have ordered within the last 90 days" do
#customer = FactoryGirl.create(:customer)
#order = FactoryGirl.create(:order, created_at: 50.days.ago, customer: #customer)
Customer.count.should == 1
Order.count.should == 1
Customer.ordered_in_90_days.should == [#customer]
end
Setting the #order.customer explicitly in your examples allows you to eliminate the factory dependency and complexity.
Sidenote
If you want to keep the association alias method in your factory, and rely on that association in other tests, I would suggest writing a separate test to verify that factory relationship instantiating correctly:
#order = create(:order)
expect(#order.customer).to be_a(Customer)
Or something like that...
I actually have this model:
class Role < ActiveRecord::Base
acts_as_authorization_role
def self.all_join_wisp
self.connection.select_all("SELECT roles.*, wisps.name AS wisp_name FROM roles LEFT JOIN wisps ON wisps.id = roles.authorizable_id")
end
end
the method all_join_wisp does almost what I want, it adds the field wisp_name, except it return a hash and not an active record object.
Is there a way to retrieve an active record object instead?
The Role model does not have belongs_to and the wisp model does not have has_many :roles , I guess it was done for flexibility or something (I did not develop the application I'm working on).
Edit: Solution implemented
Solution implemented here: https://github.com/nemesisdesign/OpenWISP-Geographic-Monitoring/blob/287861d95fffde35d78b76ca1e529c21b0f3a54b/app/models/role.rb#L25
Thanks to #house9
you can use find_by_sql - you get back what looks like an activerecord object but isn't really, it will have attributes from your query but they will all be string data types instead of the real types, often this is ok
def self.all_join_wisp
self.find_by_sql("SELECT roles.*, wisps.name AS wisp_name FROM roles LEFT JOIN wisps ON wisps.id = roles.authorizable_id")
end
then
list = Role.all_join_wisp
list.each do |x|
puts x
puts x.inspect
puts x.id # the role id
puts x.id.is_a?(Integer) # false
puts x.id.is_a?(String) # true
puts x.name # the role name
puts x.wisp_name # wisp name, NOTE: Role does not really have this attribute, find_by_sql adds it
end
model
class Role < ActiveRecord::Base
def self.do
joins("LEFT JOIN wisps w ON
w.id = roles.id").select("roles.*,
wisps.name AS wisp_name")
end
end
controller
#roles = Role.do
#wisps = #roles.map {|i| i.wisp_name}
view
<%= #wisps %>
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