Rails polymorphic association and has_many for the same model - ruby-on-rails

I have the Comment model which belongs to some other models like Post, Page etc and has_one (or belongs_to?) User model. But I need the User to be commentable too, so User has to have many Comments from other Users (this is polymorphic :commentable association) and he has to have his own Comments, written by him.
What is the best way to make an association like this? How can I read and create Comments for User in a controller if User has two different associations with Comments?
Now I do this and it's not right I guess:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :comments, as: :commentable
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
belongs_to :user
end
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :content
t.references :commentable, polymorphic: true, index: true
t.belongs_to :user
t.timestamps null: false
end
end
end

You'll want to use another name for that association.
has_many :comments, as: :commentable
has_many :commented_on, class_name: 'Comment' # you might also need foreign_key: 'from_user_id'.
See has_many's documentation online.
The foreign_key should not be needed in your case, but I'm pointing it out Just In Caseā„¢. Rails will guess "{class_lowercase}_id" by default (so user_id in a class named User).
Then you can access both associations (The class_name is explicitly needed because Rails can't find Comment from commented_on).

Related

Rails: How to multiple associations between two models

I have the following association between Reviews and Users:
Since I'm using Devise, I kept just a single Users table and identify the roles using client or seller columns (boolean).
So as you can imagine, I need to know the user that made the review and the user being "reviewed".
The first question is: Can I make use of references while creating the migration? I manually created these columns like this: t.integer :client_id, foreign_key: true and t.integer :seller_id, foreign_key: true
The second is: How can I specify the relationship in the models? I did like this has_many :reviews, foreign_key: "client_id" and has_many :reviews, foreign_key: "seller_id" but i'm not sure if it's correct.
Here's the full code of migration:
class CreateReviews < ActiveRecord::Migration[6.0]
def change
create_table :reviews do |t|
t.text :description
t.integer :rating, null: false
t.integer :client_id, foreign_key: true
t.integer :seller_id, foreign_key: true
t.timestamps
end
end
end
The User Model:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :reviews, foreign_key: "client_id"
has_many :reviews, foreign_key: "seller_id"
end
and the Review model:
class Review < ApplicationRecord
belongs_to :user
end
Rails Version: 6.0.3.2 - Ruby Version: 2.6.6
I see what you are trying to achieve.
First thing first, remove foreign_key: true in your CreateReviews migration because it has no effect, you might want to index those two columns by replacing it with index: true.
Then in your User model have two different has_many associations eg
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :client_reviews, foreign_key: "client_id", class_name: 'Review'
has_many :seller_reviews, foreign_key: "seller_id", class_name: 'Review'
end
Why two different associations? well because when you have two same associations it will always use the last association hence overriding the first one.
You might want to try it in your console and see the output, for your case if you inspect the query you will see that it is using seller_id column to find reviews if you try something like.
user = User.first
p user.reviews.to_sql
Now refactor your Review model to have something like this
class Review < ApplicationRecord
belongs_to :client, foreign_key: :client_id, class_name: 'User'
belongs_to :seller, foreign_key: :seller_id, class_name: 'User'
end
Now you can create client_reviews and seller_reviews and query each one
seller = User.create(name: 'Seller 1)
client = User.create(name: 'Client 1')
seller.seller_reviews.create(description: 'I like your product', client: client)
review = Review.first
p review.client
p review.seller
Hope it helps give the picture of what you can do.

Rails 5.2 eager load polymorphic nested

Is it possible to eager load polymorphic nested associations? How can I include doctor_profile's for Recommendation's and patient_profile's for Post's?
I'm able to call Activity.includes(:trackable).last(10) but not sure how to include the associated models past there. I've tried belongs_to :recommendation, -> { includes :patient_profile, :doctor_profile} with no luck
class Activity
belongs_to :trackable, polymorphic: true
end
class Recommendation
has_many :activities, as: :trackable
belongs_to :doctor_profile
end
class Post
has_many :activities, as: :trackable
belongs_to :patient_profile
end
with respect referenced this SO answer and comments
for your problem you can managed with foreign_type field from polymorphic table to reference which model that use it
class Activity
belongs_to :trackable, polymorphic: true
# below is additional info
belongs_to :recommendation, foreign_type: 'Recommendation', foreign_key: 'trackable_id'
belongs_to :post, foreign_type: 'Post', foreign_key: 'trackable_id'
end
and you can call it
Activity.includes(recommendation: :doctor_profile).last(10)
Activity.includes(post: :patient_profile).last(10)
Activity.includes(recommendation: :doctor_profile) means
Activity will join recommendation with foreign_type and trackable_id
and then from recommendation will join doctor_profile with doctor_profile_id
The above answer works, but the use of foreign_type isn't actually supposed to do what the commenter intended.
https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
foreign_type is used to specify the name for the column which determines the class type for the relation.
I think the intended result here is to instead use class_name to specify which table the relation is referring to. If the relation has the same name as the table, then class_name can actually be inferred (which is why the provided answer works in the first place)
In order to get the above answer to work, specifying inverse_of for the belongs_to and adding for the has_many associations got everything to work. For example:
class Activity
belongs_to :trackable, polymorphic: true
# below is additional info
belongs_to :recommendation, foreign_type: 'Recommendation', foreign_key: 'trackable_id', inverse_of: :activities
belongs_to :post, foreign_type: 'Post', foreign_key: 'trackable_id', inverse_of: :activities
end
On the Post model:
has_many :activities, inverse_of: :post
On the Recommendation model:
has_many :activities, inverse_of: :recommendation

Migration has_one and has_many

class Post < ActiveRecord::Base
has_one :owner, class_name: "User", foreign_key: "owner_id" #creator post
has_many :users #followers post
end
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :posts
end
What command line I need to perform to migrate to perform these different relationships between the User and Post tables?
Thanks
Post should belong_to:owner, because the posts table has the foreign key. Also, its #users is a bit too ambiguously named, as is User#posts.
Here are your models:
class Post < ActiveRecord::Base
belongs_to :owner, class_name: 'User', inverse_of: :owned_posts # foreign_key: :owner_id will be inferred
has_many :subscriptions
has_many :followers, through: :subscriptions, source: :user, class_name: 'User', inverse_of: :followed_posts
end
class Subscription < ActiveRecord::Base
belongs_to :post
belongs_to :user
end
class User < ActiveRecord::Base
has_many :owned_posts, class_name: 'Post', inverse_of: :owner
has_many :subscriptions
has_many :followed_posts, through: :subscriptions, source: :post, class_name: 'Post', inverse_of: :followers
end
And here are the migrations to support them:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
# ...
end
end
end
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.integer :owner_id
# ...
end
end
end
class CreateSubscriptions < ActiveRecord::Migration
def change
create_table :subscriptions do |t|
t.integer :post_id
t.integer :user_id
end
end
end
If it weren't for the 'ownership' relation, you could use a has_and_belongs_to_many relationship:
rename the subscriptions migration to posts_users (must be plurals in alphabetical order),
do away with its model entirely, and
have Post.has_and_belongs_to_many :users and User.has_and_belongs_to_many :posts.
In fact, you technically could do that, but having ambiguous names like that is bad practice.

How do I make my task assignment associations?

I have a User model, a TodoList model, which has many todoItems. My models are :
User Model
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
has_many :todo_lists
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
TodoList Model
class TodoList < ActiveRecord::Base
has_many :todo_items
belongs_to :user
end
ToItem Model
class TodoItem < ActiveRecord::Base
include AASM
belongs_to :todo_list
def completed?
!completed_at.blank?
end
#belongs_to :user
#belongs_to :friend, class_name: 'User', foreign_key: 'friend_id'
aasm :column => 'state', :whiny_transitions => false do
state :not_assigned, :initial => true
state :assigned
state :taskCompleted
end
I am trying to modify my models in such that any user can request to be assigned a taskItem and the user whom the task belongs to can accept or deny the requests. Once a an assignment request is approved, I want the task to be also associated to the user assigned to it.
How do I go about that with my model associations and relationships ? Thanks in advance for the help .
You could use an assignments association table, in a many-to-many relationship between User and TodoItem. Your association table would have an additional boolean attribute, indicating whether the item owner has accepted the request. Something like:
class TodoItem < ActiveRecord::Base
...
has_many :users, through: :assignments
...
end
For User:
class User < ActiveRecord::Base
...
has_many :todo_items, through: :assignments
...
end
And finally the association table:
class Assignment < ActiveRecord::Base
belongs_to :user
belongs_to :todo_item
end
Your migration to create the association table would be something like this:
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.belongs_to :user, index: true
t.belongs_to :todo_item, index: true
t.boolean :request_accepted, default: false, null: false
t.timestamps null: false
end
end
end

Private fields for multiple models, how to make on RoR?

I have 4 models with complex relations. 3 of them should have descriptions, that should be enable only for user who's create. In other words every user has his own description for Group (for example), or for Post, o something else. Let's talk about only one model, because others are very same. What I have:
user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable, omniauth_providers: [:vkontakte]
has_and_belongs_to_many :groups
has_many :descriptions
end
group.rb
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
has_many :descriptions, :as => :describable
accepts_nested_attributes_for :descriptions
end
description.rb
class Description < ActiveRecord::Base
belongs_to :user
belongs_to :describable, :polymorphic => true
end
table for descriptions
create_table "descriptions", force: :cascade do |t|
t.integer "user_id" -- belongs_to
t.string "content"
t.integer "describable_id"
t.string "describable_type"
end
How to display the description for group that belongs to current_user (I use devise)? How to build an update form with nested description?
I try to do it, but it's not work. I've ask question about part of problem here.
Why do you have an extra model called description?
Although it's not a problem in itself, you really don't need to have a model just for description.
--
Profile
Instead, you may wish to put the details into a profile model, or simply in the user model (there's nothing wrong with adding extra attributes to a Devise model).
We use a profile model, which gives us the ability to add as many "extra" fields as we want to the user model:
You can set it up like this:
#app/models/user.rb
class User < ActiveRecord::Base
has_one :profile
accepts_nested_attributes_for :profile
before_create :build_profile
delegate :description, :name, to: :profile, prefix: false #-> #user.description
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
This will allow you to create a single profile per user, have that profile built when the user is created, and then change as many options inside the profile as you wish.

Resources