Rails join through 2 other tables - ruby-on-rails

I am trying to join tables to get an object.
I have these models:
class Company < ApplicationRecord
has_many :users
end
class Claim < ApplicationRecord
has_many :uploads, dependent: :destroy
validates :number, uniqueness: true
belongs_to :user, optional: true
end
class User < ApplicationRecord
belongs_to :company
has_many :claims
end
Basically I want to select all claims that belong to users that belong to a company.
Somethings I have tried:
(This works but is terrible and not the rails way)
#claims = []
#company = Company.find(params[:id])
#users = #company.users
#users.each do |u|
u.claims.each do |c|
#claims.push(c)
end
end
#claims = #claims.sort_by(&:created_at)
if #claims.count > 10
#claims.shift(#claims.count - 10)
end
#claims = #claims.reverse
This is close but doesn't have all the claim data because its of the user:
#claims = User.joins(:claims, :company).where("companies.id = users.company_id").where("claims.user_id = users.id").where(company_id: params[:id]).order("created_at DESC").limit(10)
I tried this but keep getting an error:
#claims = Claim.joins(:user, :company).where("companies.id = users.company_id").where("claims.user_id = users.id").where(company_id: params[:id]).order("created_at DESC").limit(10)
error: ActiveRecord::ConfigurationError (Can't join 'Claim' to association named 'company'; perhaps you misspelled it?)
Any ideas what I should do or change?

Based on your relations, you should use
Claim.joins(user: :company)
Because the Company is accessible through the relation Claim <> User.
If you wanted to join/preload/include/eager load another relation, let's say if Claim belongs_to :insurance_company, then you would add it like this:
Claim.joins(:insurance_company, user: :company)
Similar questions:
Join multiple tables with active records
Rails 4 scope to find parents with no children
That being said, if you want to
select all claims that belong to users that belong to a company
Then you can do the following:
Claim
.joins(:user) # no need to join on company because company_id is already on users
.where(company_id: params[:id])
.order(claims: { created_at: :desc })
.limit(10)
Tada!

Related

How to structure a has_many association with a dynamic scope?

I have a users table in my db. A user can be either of type 'admin' or 'manager'.
Given the models and schema below, I would like that for each instance of 'manager' user, an 'admin' user could select one, some or all the locations of the tenant that the manager belongs to in order to select which locations the manager can have control over.
My models
class User < ActiveRecord::Base
belongs_to :tenant
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
belongs_to :tenant, inverse_of: :locations
I've tried two paths
First, trying to establish a scoped has_many association between the User and the Location models. However, I can't wrap my head around structuring this scope so that an 'admin' user could select which locations the 'manager' users can control.
Second, setting up a controlled_locations attribute in the users table. Then I set up some code so that an 'admin' user can select which locations a 'manager' can control, populating its 'controlled_locations' attribute. However, what gets saved in the database (inside the controlled_locations array) is strings instead of instances of locations.
Here's the code that I tried for the second path:
The migration
def change
add_column :users, :controlled_locations, :string, array: true, default: []
end
In the view
= f.input :controlled_locations, label: 'Select', collection: #tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}
In the users controller (inside the update method)
if params["user"]["controlled_locations"]
params["user"]["controlled_locations"].each do |l|
resource.controlled_locations << Location.find(l.to_i)
end
resource.save!
end
What I expect
First of all, I'm not quite sure the second path that I tried is a good approach (storing arrays in the db). So my best choice would be to set up a scoped association if it's possible.
In case the second path is feasible, what I would like to get is something like this. Let's say that logging in an Admin, I selected that the user with ID 1 (a manager) can control one location (Boston Stadium):
user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]
Instead, what I get after trying is this:
user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]
Instead of instances of locations, what gets saved in the array is just plain strings.
First, your code is missing the locations association in the Tenant class.
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations
Let's say the variable manager has a User record. Then the locations it can control are:
manager.tenant.locations
If you want, you can shorten this with a delegate statement.
class User < ActiveRecord::Base
belongs_to :tenant
delegate :locations, to: :tenant
then you can call this with
manager.locations
A common pattern used for authorization is roles:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def add_role(name, location)
self.roles << Role.find_or_create_by(name: name, location: location)
end
def has_role?(name, location)
self.roles.exists?(name: name, location: location)
end
end
# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
belongs_to :location
has_many :user_roles
has_many :users, through: :user_roles
validates_uniqueness_of :name, scope: :location_id
end
# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
belongs_to :role
belongs_to :user
validates_uniqueness_of :user_id, scope: :role_id
end
class Location < ApplicationRecord
has_many :roles
has_many :users, through: :roles
end
By making the system a bit more generic than say a controlled_locations association you can re-use it for different cases.
Let's say that logging in an Admin, I selected that the user with ID 1
(a manager) can control one location (Boston Stadium)
User.find(1)
.add_role(:manager, Location.find_by(name: "Boston Stadium"))
In actual MVC terms you can do this by setting up roles as a nested resource that can be CRUD'ed just like any other resource. Editing multiple roles in a single form can be done with accepts_nested_attributes or AJAX.
If you want to scope a query by the presence of a role then join the roles and user roles table:
Location.joins(roles: :user_roles)
.where(roles: { name: :manager })
.where(user_roles: { user_id: 1 })
To authenticate a single resource you would do:
class ApplicationController < ActionController::Base
protected
def deny_access
redirect_to "your/sign_in/path", error: 'You are not authorized.'
end
end
class LocationsController < ApplicationController
# ...
def update
#location = Location.find(params[:location_id])
deny_access and return unless current_user.has_role?(:manger, #location)
# ...
end
end
Instead of rolling your own authorization system though I would consider using rolify and pundit.

Rails Active Record different association results into one object

I have a Project model.
Project model has "all_users" instance method which returns all users of the project.
class Project < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships, source: :member, source_type: 'User'
has_many :teams, through: :memberships, source: :member, source_type: 'Team'
scope :all_users, -> (project) {
User.where(%{
(users.id in (select member_id from memberships where project_id = #{project.id} and member_type = 'User')) OR
(users.id in (select user_id from teams_users where team_id IN (select member_id from memberships where project_id = #{project.id} and member_type = 'Team')))
})
}
def all_users
Project.all_users(self).order(:name)
end
end
A user has many projects.
I want to make an instance method in User model to return all users of instance's all projects. Such as:
class User < ActiveRecord::Base
has_many :memberships, as: :member, dependent: :destroy
has_many :projects, through: :memberships
def colleagues
colleagues_of_user = []
projects.each do |project|
project.all_users.each do |user|
colleagues_of_user << user
end
end
teams.each do |team|
team.projects.each do |project|
project.all_users.each do |user|
colleagues_of_user << user
end
end
end
colleagues_of_user.uniq
end
end
The problem is; i want to concatenate all "project.all_users" into one object but i can't. I have to turn them into an array (to_a). But i want the results (colleagues_of_user) in one object ("ActiveRecord::Relation").
UPDATE: Another point that should be noted is;
colleagues_of_user could be:
1. Any user that is member of any projects of the current user.
2. Any user that is member of current user's teams' projects.
I have updated "colleagues" method regarding these notes. How to get all results into one ActiveRecord::Relation object? (Not an array)
Since you want colleagues_of_user to be ActiveRecord::Relation rather than an Array, I think you could do it like this:
def colleagues
colleague_ids = projects_colleague_ids + teams_projects_colleague_ids
colleagues_of_user = User.where(id: colleague_ids.flatten.uniq )
end
private
def projects_colleague_ids(projects = nil)
projects ||= self.projects
projects.includes(:users).collect{ |project| project.all_users.pluck(:id) }.flatten.uniq
end
def teams_projects_colleague_ids
teams.includes(projects: :users).collect do |team|
projects_colleague_ids( team.projects )
end.flatten.uniq
end
I think something like this should work:
def colleagues
projects.map(&:all_users)
end
You can try this with eager loading also.
Project.includes(users).map(&:all_users)
Thanks

Ruby on Rails relationship model

In Ruby on Rails 4, how do you create a many-to-many relationship inside a relationship model for a friends list such as Facebook using the has_many :through ... syntax ?? I'm a newbie and currently learning Ruby on Rails 4. I have looked at this link.
But still have a hard time grasping it.
you will need a join table that references both sides of the relations
let us say you have an relation Post and another relation Category with a many to many relationship between them you need a join table to be able to represent the relationship.
migration for a join table would be
class CreateCategoriesPosts < ActiveRecord::Migration
def change
create_table :categories_posts do |t|
t.integer :category_id
t.integer :post_id
t.timestamps
end
add_index :categories_posts, [:category_id, :post_id]
end
end
and in the models/post.rb
Class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
end
and in the models/category.rb
Class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
end
more here:
http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
I think #RAF pretty much nailed it. But to use the OP's example:
class User < ActiveRecord::Base
has_and_belongs_to_many :users_list
end
class UsersList < ActiveRecord::Base
has_and_belongs_to_many :users
end
Although at first it might seem like a User should have only one list of friends (UsersList), that might not always be the case. Think of types within the UserList model, such as: 'close friends', 'work friends', 'all friends' for example.
My advice: dig into the Rails guides. This is a concept worth learning and truly understanding (which I'm still doing :).
many-to_many relationships are a simple concept, but complex when using the database because of the way databases work. A person could have 1 to N different friends, which means that a single entry for a database would need a dynamic amount of memory for each entry, which in the db world is a no-no. So instead of creating a list of friends you would have to make a table that represents the links between friends, for example:
friendship.rb
class Friendship < ActiveRecord::Base
belongs_to :friend, foreign_key: 'friend_A' # this entry has a field called 'friend_A'
belongs_to :friend, foreign_key: 'friend_B' # this entry has a field called 'friend_B'
end
These links will represent your network of friends. However, as the two previous answers have mentioned, Rails has some nifty magic, "has_and_belongs_to_many", which will do this for you.
NOTICE: The problem here is that in my StatusesController, in the index action, the #relationship object only gets the statuses of all your friends, but does not get your own statuses. Is there a better way of approaching this? I am trying to create a view to view all statuses of users that are your friends, and your own statuses too, and so far, I can't seem to figure out how to order it chronologically, even if in my status model, i included "default_scope -> { order(created_at: :desc) } ". Any advice would be deeply appreciated
class User < ActiveRecord::Base
has_many :relationships
has_many :friends, :through => :relationships
has_many :inverse_relationships, class_name: 'Relationship', foreign_key: 'friend_id'
has_many :inverse_friends, through: 'inverse_relationships', :source => :user end
#
class Relationship < ActiveRecord::Base
# before_save...
belongs_to :user
belongs_to :friend, class_name: 'User'
end
#
class RelationshipsController < ApplicationController
def friend_request
user_id = current_user.id
friend_id = params[:id]
if Relationship.where( user_id: user_id, friend_id: friend_id, accepted: false).blank?
Relationship.create(user_id: user_id, friend_id: friend_id, accepted: false)
redirect_to user_path(params[:id])
else
redirect_to user_path(params[:id])
end
end
def friend_request_accept
# accepting a friend request is done by the recipient of the friend request.
# thus the current user is identified by to_id.
relationship = Relationship.where(user_id: params[:id], friend_id: current_user.id).first
if Relationship.exists?(relationship) and relationship.accepted == false
relationship.update_attributes(accepted: true)
end
redirect_to relationships_path
end
def friend_request_reject
relationship = Relationship.where(user_id: params[:id], friend_id: current_user.id).first
relationship.destroy
redirect_to relationships_path
end
################################
def index
#relationships_pending = Relationship.where(friend_id: current_user.id, accepted: false)
end
end
#
class StatusesController < ApplicationController
def index
#status = Status.new
#relationship = Relationship.where('friend_id = ? OR user_id = ?', current_user.id, current_user.id).
where( accepted: true)
end
def new
#status = Status.new
end
end
#

Feedback model for two rails models

I have two models - Customer and Contractors. I have setup a simple app, where they interact on an activity. Now at the end of it, I would like for them to leave each other feedbacks. Nothing complex just a database field of comment.
I am wondering what is the right model association to have here?
I tried this
class Customer
has_many :feedbacks
end
class Contractor
has_many :feedbacks
end
class Feedback
belongs_to :customer
belongs_to :contractor
end
But the problem here is identifying who commented who.
For instance, if I do
customer = Customer.find(1)
contractor = Contractor.find(1)
customer.feedbacks.create(:comment => "Contractor 1 sucks", :contractor_id => 1)
The problem is, its accessible by both contractor.feedbacks and customer.feedbacks. And I dont know who commented who now.
Any guidance is appreciated. Am i missing something?
Thanks
The way to do this would be to use polymorphic associations.
This way, you could have a commenter relationship, and a commentable relationship.
Like so:
class Customer
has_many :feedbacks, as: commenter
has_many :feedbacks, as: commentable
end
class Contractor
has_many :feedbacks, as: commenter
has_many :feedbacks, as: commentable
end
class Feedback
belongs_to :commenter, polymorphic: true
belongs_to :commentable, polymorphic: true
end
Now, Feedback will require four new columns:
commentable_type:string
commentable_id:integer
commenter_type:string
commenter_id:integer
All four should be indexed, so write your migrations appropriately. The type columns will store a String value of the model name associated ("Customer" or "Contractor").
So you can do things like:
#feedback = Feedback.find 3
#feedback.commenter
=> # Some Customer
#feedback.commentable
=> # Some Contractor
And vise versa. You would build like:
#customer = Customer.find 1
#contractor = Contractor.find 1
#feedback = Feedback.new comment: "This is a great Contractor"
#feedback.commenter = #customer # You can reverse this for a contractor giving feedback to a customer
#feedback.commentable = #contractor
#feedback.save!

How to return an ActiveRecord relation through two other has_many associations

Model:
class User < ActiveRecord::Base
has_many :requests, class_name: 'Story', foreign_key: 'requester_id'
has_many :ownerships, class_name: 'Story', foreign_key: 'owner_id'
def stories
requests | ownerships
end
end
In this case the method stories will return an array of uniq objects as I want. But I'll need to use something like User.first.stories.where("title = 'foo'") that returns an error, because it's an array, not a relation.
So what can I do to get this same results through relation allowing to use with Arel?
PS.: Im on Rails 3.1.rc6
class User < ActiveRecord::Base
def stories
Story.where "(owner_id = :id) OR (requester_id = :id)", :id => id
end
end
or this could be written even nicer if you use squeel
def stories
Story.where { (owner_id == my{id}) | (requester_id == my{id}) }
end
Only a scope returns a relation. Transforming the method as LOJ scope should help I guess.
scope :stories, select('stories.*').joins('LEFT OUTER JOIN stories ON
users.id = stories.requester_id LEFT OUTER JOIN stories ON users.id = stories.owner_id')

Resources