Manager -> Employee association one level in rails - ruby-on-rails

I want to create manager->employee association.
Manager have many employees
Employee belongs to only one manager
Manager does not have another manager
How to implement this design ?
I created something closer(code is below) but in my design manager can have other manager.
class User < ApplicationRecord
has_many :employee, class_name: "User", foreign_key: "manager_id"
belongs_to :manager, class_name: "User", foreign_key: "manager_id"
end
Any help is highly appreciated!

What you have is a really standard self-joining setup (with a few small issues). You can limit the depth of the tree by just adding a custom validation:
class User < ApplicationRecord
# optional: true is needed in Rails 5+ since belongs_to is no
# longer optional by default
belongs_to :manager, class_name: 'User',
optional: true
# pay attention to pluralization
has_many :employees, class_name: 'User',
foreign_key: 'manager_id'
# calls the custom validation we will define later
validate :manager_must_be_a_manager, unless: :manager?
def manager?
manager.nil?
end
private
# our custom validation method
def manager_must_be_a_manager
errors.add(:manager, 'is not a manager') unless manager.manager?
end
end
So now if we run:
user_1 = User.create!
user_2 = User.create!(manager: user_1)
User.create!(manager: user_2)
The third line will raise ActiveRecord::RecordInvalid: Validation failed: Manager is not a manager.

I think it's better to have one more field manager: boolean in your table. On the basis of this field you can decide whether a user is manager or not. Also you can add validations that manager_id must be blank if manager field is true.
validate :manager_id_blank_for_manager
has_many :employees, class_name: "User", foreign_key: "manager_id", dependent: :restrict_with_exception
scope :managers, ->{ where(manager: true) }
def manager?
manager
end
private
def manager_id_blank_for_manager
if manager_id.present? && manager?
errors.add(:manager_id, :must_be_blank_for_manager) # Move this error to yml file
end
end

Related

Ruby on Rails - Dividing a Class into multiple classes

I have this app: Task Manager.
I have a User.
A user can create one or more Groups -> then it becomes admin of the groups it created
At the same time a user can be just a Member (a middle table between User and Group) of a Group created by another one.
So I have that User:
has_many :groups, foreign_key: :admin_id, dependent: :destroy
has_many :groups, through: :members
And now I want to ask to the Db:
Give me the groups where user is admin
Give me the groups where user is just a user
I can most likely create a SQL query for that but I thought there would be a more Rails way of doing things.
Is there a way to do this? I can only think of having User but at the same time dividing it into 2 different subclasses like UserAdmin and NormalUser. But I am not sure on how to do that or if it is even the right approach.
Thank you in advance!
Splitting your User class into multiple classes isn't the answer as you want users to be able to have different roles in different groups. Rather there are few different solutions.
1. Add a foreign key column
If you want to have a specific one to many assocation between a user and a group you can add a separate assocation:
class AddCreatorToGroups < ActiveRecord::Migration[7.0]
def change
# note that you'll have to worry about filling this
# column if you have existing data
add_reference :groups, :founder, null: false, foreign_key: { to_table: :users }
end
end
class Group
# ...
belongs_to :founder,
class_name: 'User',
inverse_of: :groups_as_founder
end
class User
# ...
has_many :groups_as_founder,
class_name: 'Group',
foreign_key: :founder_id,
inverse_of: :founder
end
This is a good idea if you need to be able to eager load the group and just that user very efficiently. This would be set when creating the group:
def create
#group = Group.new(group_params) do |group|
group.founder = current_user
end
# ...
end
This can be combined with the other options. It won't solve the case where you want to assign multiple admins to a group.
2. Add the roles to the memberships
class AddCreatorToGroups < ActiveRecord::Migration[7.0]
def change
add_columns :memberships, :role, :integer, default: 0
end
end
class Membership < ApplicationRecord
belongs_to :group
belongs_to :member, class_name: 'User'
enum :role, {
normal: 0,
admin: 1
}
end
class Group < ApplicationRecord
has_many :normal_memberships,
class_name: 'Membership',
-> { Membership.normal }
has_many :admin_memberships,
class_name: 'Membership',
-> { Membership.admin }
has_many :normal_members,
class_name: 'User',
through: :normal_memberships
has_many :admin_members,
class_name: 'User',
through: :admin_memberships
end
class User < ApplicationRecord
has_many :normal_memberships,
class_name: 'Membership',
-> { Membership.normal }
has_many :admin_memberships,
class_name: 'Membership',
-> { Membership.admin }
has_many :groups_as_normal_member,
class_name: 'Group',
through: :normal_memberships
has_many :groups_as_admin_member,
class_name: 'Group',
through: :admin_memberships
end
This is a greatly simpliefied example where the available roles are defined through an enum on the memberships table. A more complex example could have the role defined as role_id pointing to a separate table.
This repition can be avoided somewhat by looping across Membership.roles.key.
One thing to consider is that it places a lot of responsibities into the Membership class.
3. A separate role system.
An additional alternative would be to use a separate system such as Rolify and a completely different set of tables to store the roles in a group. There are plenty of tutorials on how to use Rolify or build a role based access system from scratch if you search a bit.

how can I do inverse of self-reference with rails

I have the user model like :
has_many :users, class_name: 'User'
belongs_to :master_user, class_name: 'User', optional: true, inverse_of: :users
I Would like to find :
User.first.master_user it's ok
MasterUser.users but get an error : "NameError: uninitialized constant MasterUser"
You are getting that error because you have not defined a MasterUser model. I am guessing you only have a User model as described in your question. If you want to find the users belonging to a "master_user" then you need to find a "master_user" first, then request its users. It would look something like this:
user_with_a_master = User.where.not(master_user_id: nil).first
master = user_with_a_master.master_user
master_users = master.users
Here is an example of how to properly setup a self-referential association with a bit less confusing naming:
class User < ApplicationRecord
belongs_to :manager
class_name: 'User', # requied here since it cannot be derided from the name
optional: true,
inverse_of: :subordinates
has_many :subordinates,
class_name: 'User', # requied here since it cannot be derided from the name
foreign_key: :manager_id, # what column on the users table should we join
inverse_of: :manager
end
"Managers" here are not a separate class. While you could use single table inheritance to that purpose you should probally get the basics figured out first. Even if you did have a MasterUser class you would get NoMethodError since you're calling .users on the class and not an instance of the class - that will never work and is a very common beginner misstake.
Note that this strictly speaking would actually work without the inverse_of: option which is really just used to explicity set the two way binding in memory.
So in your case it should look like:
class User < ApplicationRecord
# class_name isn't required since it can be derided from the name
has_many :users,
foreign_key: :master_user_id,
inverse_of: :master_user
belongs_to :master_user,
class_name: 'User', # requied here since it cannot be derided from the name
optional: true,
inverse_of: :users
end
Note that the users table must have a master_user_id column.

ActiveRecord has_many and belongs_to on the same model

I've looked into this SO question but I still have trouble wrapping my head around the concept.
I have a similar setup with the linked SO question, in that I have a User class that contains both Employees and Managers. I also have another model for Role (holding the role names) and UserRole (holding which user has which role).
My requirements state that an Employee (a User whose Role is User) can only have one Manager (a User whose Role is Manager). Now, this Managering concept is an addition to the current system, and I'm not supposed to change the users table, so I'm making a new table with their own MVC.
But now I find it hard to use has_many and belongs_to like the linked question. How do I use the new model? I tried using :through but it doesn't work (for some reason).
Am I doing it wrong? Should I just add a manager_id column to users and work the solution in the linked question into my problem? Also, how do I ensure that only a User whose Role is Manager can be set as a Manager?
Note: I have to say that I'm relatively new to Rails and ActiveRecord, and even Ruby in general.
Note 2: I'm using Rails 4.2.0 if it's relevant.
Setting up a many to many system with roles is pretty straight forward:
class User
has_many :user_roles
has_many :roles, through: :user_roles
def has_role?(role)
roles.where(name: role).any?
end
end
class Role
has_many :user_roles
has_many :users, through: :user_roles
end
class UserRole
belongs_to :role
belongs_to :user
validates_uniqueness_of :role, :user
end
Just make sure you create a unique index on UserRole for role and user:
add_index :user_roles, [:role_id, :user_id], unique: true
The simplest and performant way to implement the manager requirement would be to add a
mananger_id column to users and setup a self-referencing one to many relationship:
class User
has_many :user_roles
has_many :roles, through: :user_roles
belongs_to :manager, class_name: 'User'
has_many :subordinates, foreign_key: :manager_id, class_name: 'User'
validate :authorize_manager!
def has_role?(role)
roles.where(name: role).any?
end
private
def authorize_manager!
if manager.present?
errors.add(:manager, "does not have manager role") unless manager.has_role?("manager")
end
end
end
Another way to do this would be to use resource scoped roles.
The best part is that you don't have build it yourself. There is a excellent gem created by the community called Rolify which sets you up with such as system.
Its also quite a bit more flexible than the former system, once you get a hang of it you can add roles to any kind of resource in your domain.
class User < ActiveRecord::Base
rolify
resourcify
end
---
the_boss = User.find(1)
bob = User.find_by(name: 'Bob')
# creating roles
the_boss.add_role(:manager) # add a global role
the_boss.add_role(:manager, bob) # add a role scoped to a user instance
# querying roles
bob.has_role?(:manager) # => false
the_boss.has_role?(:manager) # => true
the_boss.has_role?(:manager, bob) # => true
the_boss.has_role?(:manager, User.create) # => false
If you go with Rolify compliment it with an authorization library such as Pundit or CanCanCan to enforce the rules.
class User < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :employees, foreign_key: :manager_id, class_name: 'User'
end
Does it helps ?
Update:
class User < ActiveRecord::Base
has_one :role, through: :user_role
belongs_to :manager, -> { where(role: {name: 'manager'}), class_name: 'User'
has_many :employees, -> { where(role: {name: 'employee'}), foreign_key: :manager_id, class_name: 'User'
end

Rails: Bad Associations? [has_many , through] How to test if working?

I am struggling with an issue in my data model. I do have the following models:
class User < ActiveRecord::Base
...
has_many :claims #user-claims
has_many :claims, through: :rulings, as: :commissars
...
end
class Claim < ActiveRecord::Base
...
belongs_to :user
has_many :users, through: :rulings, as: :commissars
...
end
class Ruling < ActiveRecord::Base
belongs_to :user
belongs_to :claim
end
Error:
undefined method `commissars' for #<Claim:0xc5ac090>
Model Explanation:
User can write claims (A claim belongs to one user), and users could do the role of commissars to do the ruling of the claim (max numbers of commissars = 3 per claim).
Is there any way to fix this or improve the relationship?
This domain model requires som pretty complex relations so there is no shame in not getting it on the first try.
Lets start with user and claims:
class User < ActiveRecord::Base
has_many :claims, foreign_key: 'claimant_id',
inverse_of: :claimant
end
class Claim < ActiveRecord::Base
belongs_to :claimant, class_name: 'User',
inverse_of: :claims
end
This is a pretty basic one to many relation with a twist. Since User will have a bunch of relations to Claim we call the relation something other than the default user so that the nature of the relation is defined.
The class_name: 'User' option tells ActiveRecord to load the class User and use it to figure out what table to query and also what class to return the results as. Its needed whenever the class name cannot be directly derived from the name of the association. The option should be a string and not a constant due to the way Rails lazily resolves class dependencies.
Now lets add the commissar role. We will use ruling as the join table:
class Ruling < ActiveRecord::Base
belongs_to :claim
belongs_to :commissioner, class_name: 'User'
end
Notice that here we have a relation to User that we call commissioner for clarity. Now we add the relations to Claim:
class Claim < ActiveRecord::Base
belongs_to :claimant, class_name: 'User',
inverse_of: :claims
has_many :rulings
has_many :commissioners, through: :rulings
end
Then we need to setup the relations on the User side:
class User < ActiveRecord::Base
has_many :claims, foreign_key: 'claimant_id',
inverse_of: :claimant
# rulings as claimant
has_many :rulings, through: :claims
has_many :rulings_as_commissioner, class_name: 'Ruling',
foreign_key: 'commissioner_id'
has_many :claims_as_commissioner, through: :rulings_as_commissioner,
source: :claim
end
Note the source: :claim option where we tell ActiveRecord which party we want from the join table.
Of course for this to work we need to setup the columns and the foreign keys properly. These migrations are to create the tables from scratch but you can easily rewrite them to alter your existing tables:
class CreateClaims < ActiveRecord::Migration
def change
create_table :claims do |t|
t.belongs_to :claimant, index: true, foreign_key: false
t.timestamps null: false
end
# we need to setup the fkey ourself since it is not conventional
add_foreign_key :claims, :users, column: :claimant_id
end
end
class CreateRulings < ActiveRecord::Migration
def change
create_table :rulings do |t|
t.belongs_to :claim, index: true, foreign_key: true
t.belongs_to :commissioner, index: true, foreign_key: false
t.timestamps null: false
end
add_foreign_key :rulings, :users, column: :commissioner_id
add_index :rulings, [:claim_id, :commissioner_id], unique: true
end
end
max numbers of commissars = 3 per claim
This is not really part of the associations rather you would enforce this rule by adding a validation or an association callback.
class Ruling < ActiveRecord::Base
# ...
validate :only_three_rulings_per_claim
private
def only_three_rulings_per_claim
if claim.rulings.size >= 3
errors.add(:claim, "already has the max number of commissars")
end
end
end
See:
Rails Guides: Active Record Migrations
Rails Guides: the has_many though: relations
First, I would suggest you go back and read the Guide carefully as I believe you have fundamentally misunderstood a number of things. The as: option, for instance, does not indicate role but, rather, the presence of a polymorphic join. Also, you can't declare has_many :claims twice on the same model. Anyway, go give it another read.
But, to your question - a functional although somewhat inelegant approach might look like:
class User < ActiveRecord::Base
...
has_many :claims
has_many :claim_commissars, foreign_key: "commissar_id"
has_many :commissar_claims, through: :claim_commissars, class_name: "Claim"
# ^^^^^^^^^^^^^^^^^^^^^
# this bit may be wrong
...
end
class Claim < ActiveRecord::Base
...
belongs_to :user
has_one :ruling
has_many :claim_commissars
has_many :commissars, through: :claim_commissars
...
end
class ClaimCommissar < ActiveRecord::Base
...
belongs_to :claim
belongs_to :commissar, class_name: "User"
...
end
class Ruling < ActiveRecord::Base
...
belongs_to :claim
belongs_to :commissar, class_name: "User"
...
end
You would need to enforce your 'max 3 commissars` in the code.
This is not tested and you will likely need to fiddle with it to get it to go. But, hopefully, it sets you in a better direction.
Good luck!

association and migration between users and teams (rails)

I have this User and team model which has the following association:
user.rb
class User < ActiveRecord::Base
belongs_to :team
team.rb
class Team < ActiveRecord::Base
has_many :users
has_one :leader, class_name: "User", foreign_key: "leader_id"
belongs_to :manager, class_name: "User", foreign_key: "manager_id"
but it seems that I can't imagine representing it properly into a migration. At first, this is what I did:
class AddTeamIdToUsers < ActiveRecord::Migration
def change
add_column :users, :team_id, :integer
add_index :users, :team_id
end
end
class AddUsersToTeams < ActiveRecord::Migration
def change
add_reference :teams, :leader, index: true
add_reference :teams, :manager, index: true
end
end
for sure, what I did on AddTeamToIdUsers was a many-to-one association since a Team can have many Users, but the leader position should only be exclusive for a specific team only (same goes to members as well, they should not belong to other teams). Managers, however, can have many teams to manage. Going back to my concern, how can I represent my scenario into a migration? Or are there any adjustments I should make in my associations? After the necessary adjustments and solutions considered, will the application automatically follow the association rules upon adding/updating teams?
Your migrations look correct, but your associations are not complete:
class User < ActiveRecord::Base
belongs_to :team
has_one :leading_team, class_name: 'Team', foreign_key: 'leader_id'
has_many :managed_teams, class_name: 'Team', foreign_key, 'manager_id'
class Team < ActiveRecord::Base
has_many :users
belongs_to :leader, class_name: "User"
belongs_to :manager, class_name: "User"
And you should be all set.
Because a manager can have multiple teams, but is still "part of" the team, I'd suggest creating a join table for users and teams. We'll call it members. It will reference both user and team.
class CreateMembers < ActiveRecord::Migration
def change
create_table :members do |t|
t.references :user
t.references :team
t.timestamps
end
end
end
Then, we'll need add the members association to the User model. Users will have many members, and, because of managers, have many teams as well. I've also included a function to get the team of a worker or leader, since there's only one.
class User < ActiveRecord::Base
has_many :members
has_many :teams, through: members, dependent: destroy
validates_associated :members # More on this later
# for workers and leaders
def team
self.teams.first
end
end
Similar to the User model, we'll need to add the members association to the Team model. We'll also include a few functions to get the leader and manager of a team, and validation to make sure a team has exactly one leader and one manager.
class Team < ActiveRecord::Base
has_many :members
has_many :users, through: :members, dependent: destroy
validate :has_one_leader_and_manager
validates_associated :members # More on this later
def manager
self.users.where(type: 'manager').first
end
def leader
self.users.where(type: 'leader').first
end
def has_one_leader_and_manager
['leader', 'manager'].each do |type|
unless self.users.where(type: type).count == 1
errors.add(:users, "need to have exactly one #{type}")
end
end
end
end
Lastly, we'll set up the Member model. We can also include some validation to ensure that a team can only have one leader and one manager, and that workers and leader cannot belong to more than one team.
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :team
validate :team_has_one_leader_and_manager
# Make sure player (worker or leader) is only on one team
validates :user_id, uniqueness: true, if: :is_player?
def is_player?
['worker', 'leader'].include? user.type
end
def team_has_one_leader_and_manager
if ['leader', 'manager'].include?(user.type)
if team.users.where('type = ? AND id != ?' user.type, user_id).count.any?
errors.add(:team, "can't add another #{user.type}")
end
end
end
end
Note that with the validation methods, you may want to move them around and/or refactor them, depending on how you add users and team, and how you'll add new members. However, this answer will hopefully give you enough information to get started.

Resources