has_many through - Notes sender and receiver - ruby-on-rails

I'm working on a rails app which has three models.
class User < ApplicationRecord; end
class Share < ApplicationRecord; end
class Note < ApplicationRecord; end
create_table :users do |t|
t.timestamps
end
create_table :notes do |t|
t.integer 'user_id'
t.text 'title'
t.text 'short_description'
t.string 'name'
t.timestamps
end
create_table :shares do |t|
t.integer 'user_id'
t.integer 'receiver_id'
t.integer 'note_id'
t.timestamps
end
How can I create associations between them so, I can get
Notes which are shared by User A.
Notes which are received by User A.
Notes which are created by User A.

#Mehmet Adil İstikbal gives part of the answer so I'll try to complete it.
This is another way to do it using only associations :
class User < ApplicationRecord
has_many :created_notes, class_name: 'Note', foreign_key: :user_id
has_many :received_shares, foreign_key: :receiver_id, class_name: 'Share'
has_many :received_notes, through: :received_shares, source: :note
has_many :shares
has_many :shared_notes, through: :shares, source: :note
end
class Share < ApplicationRecord
# Optional
belongs_to :creator, class_name: 'User', foreign_key: :user_id
belongs_to :receiver, class_name: 'User', foreign_key: :receiver_id
# Mandatory
belongs_to :note
end
class Note < ApplicationRecord ; end
user_a = User.first
user_a.shared_notes
user_a.received_notes
user_a.created_notes
If you choose #Mehmet Adil İstikbal answer, please make sure to transform
user.shares.each {|share| share.note} to user.shares.map(&:note) (Use map and not each)
My answer uses has_many through association which allows you to go "through" join table.

In user model you can do like this:
has_many :shares, foreign_key: 'user_id', class_name: 'Share', dependent: :destroy
has_many :receives, foreign_key: 'receiver_id', class_name: 'Share', dependent: :destroy
and you can call like this:
User.first.shares.each {|share| share.note}
This will get all shares with first users id and all of their notes.
For receiver :
User.first.receives.each {|share| share.note}
In your share model you can also specify the opposite connection like this:
belongs_to :sender, foreign_key: 'user_id', class_name: 'User'
belongs_to :receiver, foreign_key: 'receiver_id', class_name: 'User'
With this you can call:
Share.first.receiver this will get you to user that receives this post
And for the notes which are created by user you can call:
User.first.notes
You may want to delete those dependents in order to your project.
Hope it helps

Related

Rails Association multiple foreign key same table

I am learning Rails and its Active Records, and I want to set notificatios and send them to a User and register who send it, I have something like this:
Notification Model (I don't know if it is correct to set the ':sender' and ':reciever' like I did):
class Notification < ApplicationRecord
belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
belongs_to :reciever, class_name: 'User', foreign_key: 'reciever_id'
end
User Model:
class User < ApplicationRecord
has_many :notifications
end
I can do
user.notifcations.new(:message => "New notification", :sender => User.first)
but when I save (user.save) it shows:
ActiveModel::MissingAttributeError: can't write unknown attribute 'sender_id'
Achieved by adding indexes in the model migration and keeping my models as following:
Migration:
class CreateNotifications < ActiveRecord::Migration[5.1]
def change
create_table :notifications do |t|
# Adding index
t.integer :sender_id
t.text :message
t.boolean :seen
t.boolean :deleted
t.timestamps
end
add_index :notifications, :sender_id
end
end
User Model:
class User < ApplicationRecord
has_many :notifications, foreign_key: 'user_id'
has_many :notifications_sended, class_name: 'Notification', foreign_key: 'sender_id'
end
Notification Model:
class Notification < ApplicationRecord
belongs_to :reciever, class_name: 'User', foreign_key: 'user_id'
belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
end
Also did the AddUserToNotification Migration:
rails g migration AddUserToNotification user:references
So I can do:
User.first.notifications.new(:message => "Hi", :sender => User.second)
And:
User.first.notifications # Shows the notification
User.second.notifications_sended # Shows the same notification

Rails Many to Many with Extra Column

So I have these tables:
create_table :users do |t|
t.string :username
t.string :email
t.string :password_digest
t.timestamps
end
create_table :rooms do |t|
t.string :name
t.string :password
t.integer :size
t.integer :current_size
t.timestamps
end
create_table :rooms_users do |t|
t.belongs_to :user, index: true
t.belongs_to :room, index: true
t.boolean :is_admin
t.timestamps
end
I made it so, when I call Room.find(1).users I get a list of all the users in the room. However, I also want to be able to call something like Room.find(1).admins and get a list of users that are admins (where is_admin in rooms_users is true). How would I do that?
Thank you for your time!
You want to use has_many through: instead of has_and_belongs_to_many. Both define many to many associations but has_many through: uses a model for the join rows.
The lack of a model makes has_and_belongs_to_many very limited. You cannot query the join table directly or add additional columns since the rows are created indirectly.
class User < ApplicationRecord
has_many :user_rooms
has_many :rooms, through: :user_rooms
end
class Room < ApplicationRecord
has_many :user_rooms
has_many :users, through: :user_rooms
end
class UserRoom < ApplicationRecord
belongs_to :user
belongs_to :room
end
You can use your existing schema but you need to rename the table users_rooms to user_rooms with a migration - otherwise rails will deride the class name as Rooms::User.
class RenameUsersRooms < ActiveRecord::Migration[5.0]
def change
rename_table(:users_rooms, :user_rooms)
end
end
However, I also want to be able to call something like
Room.find(1).admins and get a list of users that are admins (where
is_admin in rooms_users is true). How would I do that?
You want to use a left inner join:
User.joins(:user_rooms)
.where(user_rooms: { room_id: 1, is_admin: true })
To roll that into the class you can setup an association with a scope applied:
class Room < ApplicationRecord
has_many :user_rooms
has_many :users, through: :user_rooms
has_many :user_room_admins, class_name: 'UserRoom', ->{ where(is_admin: true) }
has_many :user_room_admins, through: :user_rooms,
class_name: 'User',
source: :user
end
You can define a proc in the has_many relation to set SQL clauses, like ORDER or WHERE:
# room.rb
has_many :rooms_users, class_name: 'RoomsUser'
has_many :users, through: :rooms_users
has_many :admins,
proc { where(rooms_users: { is_admin: true }) },
through: :rooms_users,
class_name: 'User',
source: :users
# user.rb
has_many :administrated_rooms,
proc { where(rooms_users: { is_admin: true }) },
through: :rooms_users,
class_name: 'Room',
source: :rooms
You can simplify this with a simple scope defined in the RoomsUser model, something like:
# rooms_user.rb
scope :as_admins, -> { where(is_admin: true) }
And use it in the proc:
# user.rb
has_many :administrated_rooms,
proc { as_admins },
through: :rooms_users,
class_name: 'Room',
source: :rooms
source option explained:
With source: :users, we're telling Rails to use an association called :users on the RoomsUser model (as that's the model used for :rooms_users).
(from Understanding :source option of has_one/has_many through of Rails)

Rails Association: 2 occurrences of same model

I have a Consultation model that has a post_consultant and a consultant. Both post_consultant and consultant are references to the Employee model. So you could say:
Model
Class Consultation < ActiveRecord::Base
has_one :employee # for consultant
has_one :employee # for post_consultant
end
Migration
create_table "consultations", force: :cascade do |t|
t.boolean "showed_up"
t.boolean "signed_up"
t.integer "client_id"
t.integer "consultant_id"
t.integer "post_consultant_id"
end
How am I supposed to write that?
Correct Model:
class Consultation < ActiveRecord::Base
belongs_to :consultant, class_name: "Employee", foreign_key: "consultant_id"
belongs_to :post_consultant, class_name: "Employee", foreign_key: "post_consultant_id"
end
Class Consultation < ActiveRecord::Base
belongs_to :consultant, :class_name => "Employee", :foreign_key=> "consultant_id", dependent: :destroy
belongs_to :post_consultant, :class_name=>"Employee", :foreign_key=> "post_consultant_id", dependent: :destroy
end
You can define multiple relation referring to same model.
Class Consultation < ActiveRecord::Base
has_one :consultant, class_name: 'Employee', foreign_key: :consultant_id
has_one :post_consultant, class_name: 'Employee', foreign_key: :post_consultant_id
end
Note: mention whichever foreign key you are using for each association using syntax above.

Has one and has many of itself

I have a user instance that has many invitees but only one inviter.
I am trying to access the inviter instance associated with that user and also his invitees.
i.e:
user.inviter #=> return another user instance.
user.invitees #=> return a collection on user instances
User.rb
class User < ActiveRecord::Base
has_one :inviter, class_name: Invitation, foreign_key: :invitee_id
has_many :invitees, class_name: Invitation, foreign_key: :inviter_id
end
Invitation.rb
class Invitation < ActiveRecord::Base
belongs_to :inviter, class_name: User, foreign_key: :inviter_id
belongs_to :invitee, class_name: User, foreign_key: :invitee_id
end
migration
class CreateInvitations < ActiveRecord::Migration
def change
create_table :invitations do |t|
t.references :inviter, references: :user, index: true
t.references :invitee, references: :user, index: true
t.foreign_key :users, column: :inviter_id
t.foreign_key :users, column: :invitee_id
t.timestamps
end
end
end
This works half of the way because if I call user.inviter on a user that has an inviter it will return the invitation instance but not the user like I would like. Same for user.invitees returns a collection on invitation instances.
Do y'all have an idea of how to make it work ?
Your should use through option like this:
class User < ActiveRecord::Base
has_one :invitation, inverse_of: :inviter
has_one :inviter, through: :invitation
has_many :invitations, inverse_of: :invitee
has_many :invitees, through: :invitations
end
class Invitation < ActiveRecord::Base
belongs_to :inviter, class_name: User, inverse_of: :invitation
belongs_to :invitee, class_name: User, inverse_of: :invitations
end
user.invitees will give collection of invitation records.
Using IN query into User model with all invitee_id which reference to user model will give you collection of users.
user_ids = user.invitees.map(&:invitee_id)
User.where(id: user_ids)

Rails has_and_belongs_to_many relations with two types of Users and one type of Table

I have a problem related with this association. A pasted code is better than any title:
table.rb
class Table < ActiveRecord::Base
has_and_belongs_to_many :clients, class_name: 'User'
has_and_belongs_to_many :managers, class_name: 'User'
end
user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :tables
end
migration - join table
class UsersToTable < ActiveRecord::Migration
def change
create_table :tables_users, id: false do |t|
t.references :user, as: :client
t.references :user, as: :manager
t.references :table
end
end
end
Problem
tab = Table.new
tab.save
tab.clients.create
tab.clients.create
tab.clients.create
tab.managers.create
tab.managers.size # == 4
tab.clients.size # == 4
When I creating associated Objects(Users) they all are linked to both clients and managers.
I want to be able to create them separately - When creating a client - only number of clients rise, when creating manager, only number of managers rise.
In other words I want this:
tab.managers.size # == 1
tab.clients.size # == 3
Could you please help?
has_and_belongs_to_many :stuff, class_name: 'StuffClass' is just DSL for:
has_many "<inferred_join_table_name>"
has_many :stuff, through: "<inferred_join_table_name>"
It seems that since clients and managers are names for Users, the inferred join table get's to be "TablesUsers", and that is not right.
Try specifyng the join table for both and using different join tables for each relationship:
class Table
has_many :tables_clients
has_many :clients, through: :tables_clients
has_many :tables_managers
has_many :clients, through: :tables_managers
end
class TablesClients
belongs_to :client, class_name: 'User'
belongs_to :table
end
create_table :tables_clients, id: false do |t|
t.references :client, index: true
t.references :table, index: true
end
# and the same for tables_managers
Then the user belongs to Tables in too different ways:
class User
has_many :client_tables_users, class_name: 'TablesUsers', foreign_key: :client_id
has_many :tables_as_client, through: :client_tables_users, source: :table
has_many :managed_tables_users, class_name: 'TablesUsers', foreign_key: :manager_id
has_many :managed_tables, through: :managed_tables_users, source: :table
end

Resources