Rails 4.2 has_many through - ruby-on-rails

Problem
I have two models a client and a user.
A client can have many administrators and a user can be the administrator of many clients.
I found a couple of people that suggest using has_many :through is the better way to model this relationship in my situation versus has_and_belongs_to_many.
User model
class V1::User < ActiveRecord::Base
has_many :administrators,
class_name: 'V1::ClientAdministrator'
has_many :clients,
through: :administrators
class_name: 'V1::Clients'
Client model
class V1::Client < ActiveRecord::Base
has_many :users,
class_name: "V1::User"
has_many :administrators,
through: :users,
class_name: "V1::ClientAdministrator"
validates :administrators,
length: { minimum: 1}
ClientAdministrator model
class V1::ClientAdministrator < ActiveRecord::Base
belongs_to :v1_user
belongs_to :v1_client
end
Demo using rails c
u = V1::User.create!(name: 'test_user')
c = V1::Client.new(name: 'test_client')
c.administrators << u
ActiveRecord::AssociationTypeMismatch: V1::ClientAdministrator(#70347494104080) expected, got V1::User(#70347494299440)
Before switching to has_many :through I was successfully using has_and_belongs_to_many:
class V1::Client < ActiveRecord::Base
has_and_belongs_to_many :administrators,
-> { uniq },
join_table: :v1_client_administrators,
foreign_key: "v1_client_id",
association_foreign_key: "v1_user_id",
class_name: "V1::User"
The problem with this approach was I was not able to do any validation on the association such as before_destroy make sure there is still one administrator. Additionally, it's likely that I'll add metadata to that relationship in the future.
The ASK
How can I get / set the administrators of the client?
Is there any way that client.administrators would be an array of users instead of forcing me to client.administrators.each { |admin| admin.user} to get access to the user? (If I eventually add metadata this probably can't doesn't make sense)
Is there a way to restrict the use of client.users in favor of client.administrators?
Do model concerns help here?
Is this the right approach for ensuring there is always at least one administrator?

I believe here is what you're looking for, please namespace appropriately:
class User
has_many :clients_users
has_many :clients, through: :clients_users
end
class Client
has_many :clients_users
has_many :administrators, through: :clients_users, source: :user
validates :adminstrators, presence: true # I think this should ensure at least one admin
end
class ClientsUser
belongs_to :client
belongs_to :user
end
Client.first.administrators # fetch all adminstrators
Client.first.adminstrators << User.first # add an administrator

Related

How to find all projects where client & user involved in Ruby on Rails

At first, I want to show my associated models as like
#=> client.rb
class Client < ApplicationRecord
has_many :client_assignments, dependent: :destroy
has_many :projects, through: :client_assignments
end
#=> client_assignment.rb
class ClientAssignment < ApplicationRecord
belongs_to :project
belongs_to :client
end
#=> project.rb
class Project < ApplicationRecord
has_many :client_assignments, dependent: :destroy
has_many :clients, through: :client_assignments
validates :client_assignments, presence: true
has_many :user_assignments, dependent: :destroy
has_many :users, through: :user_assignments
validates :user_assignments, presence: true
end
#=> user.rb
class User < ApplicationRecord
has_many :user_assignments, dependent: :destroy
has_many :projects, through: :user_assignments
end
#=> user_assignment.rb
class UserAssignment < ApplicationRecord
belongs_to :project
belongs_to :user
end
The concept is "A project involved with a client & current_user".
I'm struggling for that how to find current_user involved projects where client is specific, For example: I have a client_id & which is 2 so how I find all projects for this client where matching current_user.id.
I don't know how I describe this.
Please let me know if you have any confusion.
I haven't tested this, but I feel like something like this should work:
current_user.projects.joins(:clients).where('clients.id = ?', client_id)
you want all the projects associated w/ the current user and then find all the clients associated w/ those projects and filter by the specific client_id.
This should do the same thing and may be faster because it avoids a join:
current_user.projects.joins(:client_assignments).where(clients_id:client_id)

Rails ActiveRecord Associations that include "owner" role

I’m creating a chore tracker app as my first Rails project and I’m wondering if the associations I've created make sense or could be improved. Here are the details of the app:
A user makes a chore list and becomes the “owner”(i.e. “admin”) of
that list.
The owner can create/edit chores for the list. They can also “approve" other users to complete tasks on the list. These users can ONLY complete tasks.
The owner, along with the usual admin abilities, can also complete
tasks on a list that they own.
Owners can own multiple lists. Users can be approved to complete
tasks on multiple lists.
And here are the relationships I’ve roughed out that I’m looking for feedback on:
(Model)User
has_many :lists
has_many :owners, class_name: “List”, foreign_key: “list_id"
has_many :chores, through: :lists
(Model)List
has_many :users
has_many :chores
belongs_to :owner, class_name: “User”, foreign_key: “owner_id"
(Model)User_List
belongs_to :user
belongs_to :list
(Model)Chore
belongs_to :list
has_many :users, through: :lists
Any red flags? Thank you in advance!
I would do something like this:
# user.rb
class User
has_many :lists_users
has_many :lists, through: :lists_users
has_many :chores, through :lists
end
# list.rb
class List
has_many :lists_users
has_many :users, through: :lists_users
has_many :chores
end
# lists_user.rb
class ListsUser
belongs_to :user
belongs_to :list
# field (Representational, add it in database)
:user_role
- member (can view)
- collaborator (can mark complete)
- owner (can do everything)
end
# chore.rb
class Chore
belongs_to :list
end
NOTE: I changed the association model to ListsUser as per rails naming conventions.
Please let me know if i missed something.

How should I approach this relations in ruby?

I've been going back and forward on this and I would like some advices.
I have "User" that can be part of many "Organizations", and for each one they can have many "Roles". (actually I have this scenario repeated with other kind of users and with something like roles, but for the sake of the example I summed it up).
My initial approach was doing a Table with user_id, organization_id and role_id, but that would mean many registers with the same user_id and organization_id just to change the role_id.
So I thought of doing an organization_users relation table and an organization_users_roles relation. The thing is, now I don't exactly know how to code the models.
class Organization < ActiveRecord::Base
has_and_belongs_to_many :users, join_table: :organization_users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :organizations, join_table: :organization_users
end
class OrganizationUser < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :organizations
has_many :organization_user_roles
has_many :roles, through: :organization_user_roles
end
class OrganizationUserRole < ActiveRecord::Base
has_and_belongs_to_many :roles
has_and_belongs_to_many :organization_users
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :organization_user_roles
end
If for example I want to get: ´OrganizationUser.first.roles´ I get an error saying: PG::UndefinedTable: ERROR: relation "organization_user_roles" does not exist
How should I fix my models?
You should use a much simpler approach. According to your description, Roles is actually what connects Users to Organizations and vice-versa.
Using the has_many and has_many :through associations, this can be implemented like the following:
class User < ActiveRecord::Base
has_many :roles, inverse_of: :users, dependent: :destroy
has_many :organizations, inverse_of: :users, through: :roles
end
class Organization < ActiveRecord::Base
has_many :roles, inverse_of: :organizations, dependent: :destroy
has_many :users, inverse_of: :organizations, through: :roles
end
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
belongs_to :organization, inverse_of: :roles
end
If you wish to preserve roles when you destroy users or organizations, change the dependent: keys to :nullify. This might be a good idea if you add other descriptive data in your Role and want the role to remain even though temporarily vacated by a user, for example.
The has_many :through association reference:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
To add to jaxx's answer (I upvoted), I originally thought you'd be best looking at has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :positions
has_many :organizations, through: :positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
#columns id | user_id | organization_id | role_id | etc | created_at | updated_at
belongs_to :user
belongs_to :organization
belongs_to :role
delegate :name, to: :role #-> #position.name
end
#app/models/organization.rb
class Organization < ActiveRecord::Base
has_many :positions
has_many :users, through: :positions
end
#app/models/role.rb
class Role < ActiveRecord::Base
has_many :positions
end
This will allow you to call the following:
#organization = Organization.find x
#organization.positions
#organization.users
#user = User.find x
#user.organizations
#user.positions
This is much simpler than your approach, and therefore has much more ability to keep your system flexible & extensible.
If you want to scope your #organizations, you should be able to do so, and still call the users / positions you need.
One of the added benefits of the code above is that the Position model will give you an actual set of data which can be shared between organizations and users.
It resolves one of the main issues with jaxx's answer, which is that you have to set a role for every association you make. With my interpretation, your roles can be set on their own, and each position assigned the privileges each role provides.
If the user can have many Roles for a single organisation,
and OrganizationUser represents this membership,
than, yes, you need another table for organization_user_roles.
You need to explicitly create it in the database (normally with a migration)
To not get confused, try to find a nice name for OrganisationUser, like employment, membership, etc.

Rails: three-way has_many/through relationship: prevent duplicate join items creation

Here is an extract of the models I have:
class User < ActiveRecord::Base
has_many :participations
has_many :groups, through: :participations
has_many :subgroups, through: :participations
end
class Group < ActiveRecord::Base
has_many :participations
has_many :users, through: :participations
has_many :subgroups
end
class Subgroup < ActiveRecord::Base
has_many :participations
has_many :users, through: :participations
end
class Participation < ActiveRecord::Base
belongs_to: :user
belongs_to: :group
belongs_to: :subgroup
validates :user, presence: true
validates :group, presence: true
# Subgroup can be empty, as long as user as not been placed.
# There should be only one participation per couple User:Group
validates_uniqueness_of :group_id, :scope => [:user_id]
# Also has a state-machine, describing the participation status.
end
Explanation: groups are split in subgroups, users select the group they join, but not the subgroup, which is selected later by an administrator.
When a User is added to a Group (group_a.users << user_a), a Participation is automatically created by ActiveRecord.
I would like the same participation to be reused when the same User is added to a Subgroup of that Group (subgroup_1.users << user_a with subgroup_1 a Subgroup of group_a's).
What happens actually is ActiveRecord trying to create a new Participation record, which conflicts with the previously created one (validates_uniqueness_of :group_id, :scope => [:user_id]
fires an error).
Is there anyway I could make this work? I tried hooking before_validation, before_save, and some other stuff, but every attempt failed.
Maybe there is a better way to actually model this relationship?
Any help is welcome.
Thank you,
David
You could DRY up all of your code by instead calling
class User < ActiveRecord::Base
has_many :participations
has_many :groups, through: :participations
has_many :subgroups, through: :groups # HMT -> HMT
end
Would this solve your problem? This probably won't scale, but we'll worry about that later :).

Rails has_many :though STI

Lets start with code that may go like this:
class User < ActiveRecord::Base
has_many :photos
has_many :submitted_photos
has_many :posted_photos
has_many :handshakes, through: :photos
end
class Photo < ActiveRecord::Base
belongs_to :user
end
class PostedPhoto < Photo
has_many :handshakes, inverse_of: :poster, foreign_key: 'poster_id'
end
class SubmittedPhoto < Photo
has_many :handshakes, inverse_of: :submitter, foreign_key: 'submitter_id'
end
class Handshake < ActiveRecord::Base
belongs_to :submitter, class_name: 'SubmittedPhoto'
belongs_to :poster, class_name: 'PostedPhoto'
end
The STI part and associations between photos and handshakes work fine. The problem is getting all user's handshakes through his photos.
Using the code above Rails will obviously complain that model Photo does not have an association called handshakes. Is there any facility in AR that would allow me to specify a relation of this kind? If not, what query would you write to get them?
You could use in User:
has_many :submitted_handshakes, through: :submitted_photos, source: :handshakes
has_many :posted_handshakes, through: :posted_photos, source: :handshakes
I understand this works as is you had:
user.submitted_handshakes ----> user.submitted_photos.handshakes
Obviously handshakes method does not exist in submitted_photos, but AR made this for you by joining tables.
I ended up using the following code, looked like an easiest option to solve this problem.
class User
def handshakes
Handshake.where('submitter_id IN (?) OR poster_id IN (?)', submitted_photo_ids, posted_photo_ids)
end
end

Resources