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...
Related
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}%")
)
I have a model Camera in which
belongs_to :user, :foreign_key => 'owner_id', :class_name => 'EvercamUser'
i have asscociation like this. when i do Camera.first
#<Camera id: 6, created_at: "2013-12-12 17:30:32", updated_at: "2015-11-19 10:19:33", exid: "dublin-rememberance-floor2", owner_id: 4, is_public: true
i can get owner id, is there any way to create such function that , along side getting owner id, i can get the data which linked with this id for example at id = 4
#<EvercamUser id: 4, created_at: "2013-12-12 16:43:46", updated_at: "2015-04-16 15:23:19", firstname: "Garrett", lastname: "Heaver", username: "garrettheaver"
this user is present, what if when i do Camera.first then instead of OnwerID, how can i get the owners Name?
Any help will be appreciated!
how can i get the owners Name
You'd call the associative object on the Camera object:
#camera = Camera.find x
#user = #camera.user
#user.name #-> outputs name of associated user object
... this will allow you to call the attributes of the child object on it: #camera.user.name or #camera.user.email, etc
Off topic, but I always include a reference to delegate for this type of issue; it avoids the law of demeter (where you're using more than one point to access data).
This would allow you to use:
#app/models/camera.rb
class Camera < ActiveRecord::Base
belongs_to :user, foreign_key: :owner_id, class_name: 'EvercamUser'
delegate :name, to: :user, prefix: true #-> #camera.user_name
end
#camera = Camera.find x
#camera.user_name #-> outputs the user's name on the camera object (not user object)
To give you some context, Rails uses ActiveRecord to invoke/create objects for you.
In line with the object orientated nature of Rails, ActiveRecord is known as an ORM (Object Relationship Mapper). This basically allows you to create an object through ActiveRecord, and if it is associated to another (as Rails does with its associations), it will append the associated object onto the parent.
Thus, when you're asking about calling owner_id, you're referring to the foreign_key of the association (the database column which joins the two tables together):
What you need is to reference the associated object, which I've detailed above.
What about using join here?
Camera.all.joins(:evercamusers)
Camera.where(:id => 1).joins(:users).first
Note: I'm a bit unsure if the correct parameter should be ":users" or ":evercamusers"
http://apidock.com/rails/ActiveRecord/QueryMethods/joins
You could also add methods to your class to do this.
class Camera < ActiveRecord::Base
belongs_to :user, :foreign_key => 'owner_id', :class_name => 'EvercamUser'
def firstname
self.user.firstname
end
end
When you try to output data from Camera like this:
#<Camera id: 6, created_at: "2013-12-12 17:30:32", updated_at: "2015-11-19 10:19:33", exid: "dublin-rememberance-floor2", owner_id: 4, is_public: true
It won't show. But if you call the method like this, it should work:
Camera.first.firstname # "Garrett"
Also, if JSON is acceptable you could override the as_json method.
def as_json(options={})
{ :firstname => self.user.firstname }
end
Then call it with
Camera.first.as_json
If you need to do it with all, simply loop it
Camera.all.each { |c| puts c.firstname }
I'm seeing some weird behaviour in my models, and was hoping someone could shed some light on the issue.
# user model
class User < ActiveRecord::Base
has_many :events
has_and_belongs_to_many :attended_events
def attend(event)
self.attended_events << event
end
end
# helper method in /spec-dir
def attend_events(host, guest)
host.events.each do |event|
guest.attend(event)
end
end
This, for some reason inserts the event with id 2 before the event with id 1, like so:
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 2, name: "dummy-event", user_id: 1>, #<Event id: 1, name: "dummy-event", user_id: 1>
But, when I do something seemlingly random - like for instance change the attend_event method like so:
def attend_event(event)
self.attended_events << event
p self.attended_events # random puts statement
end
It gets inserted in the correct order.
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 1, name: "dummy-event", user_id: 1>, #<Event id: 2, name: "dummy-event", user_id: 1>
What am I not getting here?
Unless you specify an order on the association, associations are unordered when they are retrieved from the database (the generated sql won't have an order clause so the database is free to return things in whatever order it wants)
You can specify an order by doing (rails 4.x upwards)
has_and_belongs_to_many :attended_events, scope: -> {order("something")}
or, on earlier versions
has_and_belongs_to_many :attended_events, :order => "something"
When you've just inserted the object you may see a different object - here you are probably seeing the loaded version of the association, which is just an array (wrapped by the proxy)
Maybe the title is confusing, but I didn't know how to explain my doubt.
Say I have the following class methods that will be helpful in order to do chainings to query a model called Player. A Player belongs_to a User, but if I want to fetch Players from a particular village or city, I have to fetch the User model.
def self.by_village(village)
joins(:user).where(:village => "village")
end
def self.by_city(city)
joins(:user).where(:city => "city")
end
Let's say I want to fetch a Player by village but also by city, so I would do...
Player.by_city(city).by_village(village).
This would be doing a join of the User twice, and I don't think that is correct.. Right?
So my question is: What would be the correct way of doing so?
I haven't tried that, but I would judge the answer to your question by the actual sql query ActiveRecord generates. If it does only one join, I would use it as you did, if this results in two joins you could create a method by_village_and_city.
OK. Tried it now:
1.9.2p290 :022 > Player.by_city("Berlin").by_village("Kreuzberg")
Player Load (0.3ms) SELECT "players".* FROM "players" INNER JOIN "users" ON "users"."id" = "players"."user_id" WHERE "users"."city" = 'Berlin' AND "users"."village" = 'Kreuzberg'
=> [#<Player id: 1, user_id: 1, created_at: "2012-07-28 17:05:35", updated_at: "2012-07-28 17:05:35">, #<Player id: 2, user_id: 2, created_at: "2012-07-28 17:08:14", updated_at: "2012-07-28 17:08:14">]
So, ActiveRecors combines the two queries, does the right thing and I would use it, except:
I had to change your implementation though:
class Player < ActiveRecord::Base
belongs_to :user
def self.by_village(village)
joins(:user).where('users.village' => village)
end
def self.by_city(city)
joins(:user).where('users.city' => city)
end
end
and what you're doing is usually handled with parameterized scopes:
class Player < ActiveRecord::Base
belongs_to :user
scope :by_village, lambda { |village| joins(:user).where('users.village = ?', village) }
scope :by_city, lambda { |city| joins(:user).where('users.city = ?', city) }
end
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