Multiple polymorphic associations on the same object - ruby-on-rails

I have a Flag model which is joined to multiple other objects with FlagInstance and a polymorphic flaggable on that table:
table 'flag_instances'
flag_id
flaggable_id
flaggable_type
.....
With has many_through I'm able to fetch any flaggable object like user.flags which is great.
However I'm trying to flag objects with errors and the notify other objects so I've added
table 'flag_instances'
flag_id
flaggable_id
flaggable_type
notifiable_id
notifiable_type
.....
The problem is, a User can have a flag and can be notified of a flag. So user.flags isn't specific enough to show me which is a flag and which is a notification of a flag.
I think I need to change the relationships:
user.rb
has_many :flag_instances, as: :flaggable, dependent: :destroy
has_many :flags, through: :flag_instances
has_many :flag_instances, as: :notifiable, dependent: :destroy
has_many :flags, through: :flag_instances
But I'm not sure what to change them to. Can someone please suggest a solution?
Note: both flags and notifications of flags can belong to multiple objects, so they both need to remain polymorphic.
Thanks

Association for notifiable needs to be changed. In this case user.rb:
has_many :flag_instances, as: :flaggable, dependent: :destroy
has_many :flags, through: :flag_instances
has_many :notifiables, as: :notifiable, dependent: :destroy, class_name: 'FlagInstance'
has_many :notifications, through: :notifiables, class_name: 'Flag'
Note: You might also need to provide foreign_key in case Rails association is not able to pick up the key itself.

Each association must have a unique name - otherwise the later definition will just overwrite the former.
Here the third line overwrites the first line:
has_many :flag_instances, as: :flaggable, dependent: :destroy
has_many :flags, through: :flag_instances
has_many :flag_instances, as: :notifiable, dependent: :destroy
To reference the correct associations we would need to setup the user model as so:
class User < ApplicationRecord
has_many :flag_instances_as_flaggable,
as: :flaggable
class_name: 'FlagInstance'
has_many :flags_as_flaggable,
through: :flag_instances_as_flaggable,
source: :flag
has_many :flag_instances_as_notifiable,
as: :notifiable
class_name: 'FlagInstance'
has_many :flags_as_notifiable,
through: :flag_instances_as_notifiable,
source: :flag
end
In your case you might want to use concerns to keep it dry:
module Flaggable
extend ActiveSupport::Concern
included do
has_many :flag_instances_as_flaggable,
as: :flaggable,
class_name: 'FlagInstance'
has_many :flags_as_flaggable,
through: :flag_instances_as_flaggable,
source: :flag
end
end
module Notifiable
extend ActiveSupport::Concern
included do
has_many :flag_instances_as_notifiable,
as: :notifiable,
class_name: 'FlagInstance'
has_many :flags_as_notifiable,
through: :flag_instances_as_notifiable,
source: :flag
end
end
class User < ApplicationRecord
include Flaggable
include Notifiable
end

Related

Rails AMS, specify different serializers in polymorphic list

I implemented a multi-table search using pg_search, and then serialize the result with Active Model Serializer (version 0.10) - it works fine, but AMS uses the default serializer for each of the types returned.
Here's the serializer:
class SearchBarSerializer < ApplicationSerializer
attributes :searchable_type
belongs_to :searchable
end
Thus, for example, when serializing the returned objects from pg_search, if the relevant object is a "User", then AMS uses UserSerializer. If the relevant type is a league, then AMS uses LeagueSerializer.
That's fine - but I would like to use a different serializer for each type. This is for a search bar, and so I only care about a much smaller amount of data than the full standard serializer. (EDIT: the standard serializers serialize all attributes and associations for each of the User and League models, which can be seen below. Each model is somewhat significantly large, and for the purposes of just a search, I really only need each model's name and id, and perhaps some other smaller data for each type)
Is there some way that I can specify which serializer to use depending on the object?
Thank you!
EDIT:
User Model:
class User < ActiveRecord::Base
include PgSearch
#################### Associations
has_and_belongs_to_many :roles
belongs_to :profile_page_visibility, optional: true # the optional part is just for when user's are created.
has_and_belongs_to_many :leagues, class_name: "Leagues::League", join_table: "users_leagues_leagues"
has_one :customer, class_name: "Payments::Customer", dependent: :destroy
has_many :unpaid_charges, class_name: "Payments::UnpaidCharge", dependent: :destroy
has_many :charges, class_name: "Payments::Charge", dependent: :destroy
has_many :cards, class_name: "Payments::Card", dependent: :destroy
has_many :league_join_requests, class_name: "Leagues::JoinRequest", dependent: :destroy
has_many :notifications, class_name: "Notification", foreign_key: :recipient_id
has_many :league_invitations, class_name: "Leagues::Invitation", dependent: :destroy
has_many :teams, class_name: "Leagues::Team"
has_many :divisions, class_name: "Leagues::Division" # Can act as division commissioner
has_many :conferences, class_name: "Leagues::Conference" # Can act as conference commissioner
League Model:
class Leagues::League < ApplicationRecord
enum pay_level: [ :basic, :custom, :premium ]
include PgSearch
#################### Associations
has_and_belongs_to_many :users, class_name: "User", join_table: "users_leagues_leagues"
has_and_belongs_to_many :commissioners, class_name: "User", join_table: "commissioners_leagues_leagues"
belongs_to :commissioner, class_name: "User", foreign_key: :commissioner_id, optional: true
has_and_belongs_to_many :feature_requests, class_name: "FeatureRequest", join_table: "feature_requests_leagues_leagues"
has_many :join_requests, class_name: "Leagues::JoinRequest", dependent: :destroy
has_many :invitations, class_name: "Leagues::Invitation", dependent: :destroy
has_many :notifications, class_name: "Notification", as: :notifiable_subject, dependent: :destroy
has_many :teams, class_name: "Leagues::Team", dependent: :destroy
has_many :conferences, class_name: "Leagues::Conference", dependent: :destroy
has_many :divisions, class_name: "Leagues::Division", dependent: :destroy
If you want to define a specific serializer lookup for your associations, you can override the ActiveModel::Serializer.serializer_for method to return a serializer class based on defined conditions.
For your case, it might look something like:
class SearchBarSerializer < ApplicationSerializer
attributes :searchable_type
belongs_to :searchable
class << self
def serializer_for(model, options)
return TinyUserSerializer if model.class == User
return TinyLeagueSerializer if model.class == Leagues::League
super
end
end
end

Trying to 'alias' a polymorphic has_many relationship

I have a User model:
class User < ActiveRecord::Base
has_many :tracks, dependent: :destroy
has_many :tracked_locations, through: :tracks, source: :tracking, source_type: 'Location'
and a Track model (think of it as 'following'):
class Track < ActiveRecord::Base
belongs_to :user
belongs_to :tracking, polymorphic: true
end
The idea here is I will have many models to track / follow so I am using polymorphism. For example I have a Location model:
class Location < ActiveRecord::Base
has_many :tracks, :as => :tracking, :dependent => :destroy
has_many :users, through: :tracks
Now in the console Location.first.users works fine along with User.first.tracked_locations.
Now I will be adding another polymorphic relationship along the lines of Flagged. The user can 'flag' another model with a note etc. So if I add has_many :users, through: :flagged to the Location model for example I need to differentiate between tracking users and flagged users.
I tried:
has_many :tracking_users, through: :tracks, source: :tracking, source_type: 'User'
but I get:
NoMethodError: undefined method `evaluators_for' for #<Location:0x007ff29e5409c8>
Can I even do this or am I missing something simple here?
UPDATE
Based on the answer below I figured it out:
has_many :tracking_users, through: :tracks, class_name: "User", foreign_key: "user_id", source: :user
I'm not 100% on this, but you could try:
has_many :tracking_users, through: :tracks, class_name: "User", foreign_key: "user_id", source: :user
Or you could also just create a class method and do it by hand.
def self.tracking_users
user_ids = tracks.collect(&:user_id)
User.where(id: user_ids)
end
edit: Had a brainfart, changed the "source" up there to :user. That tells what table to actually do the lookup in with the other attribute you've provided. of course it wouldn't be in :tracks

Accessing associated objects through a specific table

I have the following:
class User < ActiveRecord::Base
has_many :saved_courses, dependent: :destroy
has_many :courses, through: :saved_courses
has_many :course_completions
has_many :courses, through: :course_completions
end
And its course counterpart:
class Course < ActiveRecord::Base
has_many :course_completions
has_many :users, through: :course_completions
has_many :saved_courses, dependent: :destroy
has_many :users, through: :saved_courses
end
Based on these relationships, how do I get an array of all of the courses that a #user has saved?
As in, when I do #user.courses, I want it grab its saved courses, but not completed courses. I tried to do #user.saved_courses.courses but that doesn't seem like a valid operation. And #user.saved_courses.to_a simply returns an array of saved_courses, whereas I need to go a step further from there and grab the courses that those saved_course items represent.
Bonus points: I'm trying to do this in the shortest, most elegant way possible. For example, I don't want to manually iterate over #user.saved_courses.to_a, find the courses and push them into an array.
Having two associations named the same thing isn't going to work. You're going to need to do something the effect of
class User < ActiveRecord::Base
has_many :saved_courses, dependent: :destroy
has_many :saved_course_courses, through: :saved_courses, source: :course
has_many :course_completions, dependent: :destroy
has_many :course_completion_courses, through: :course_completions, source: :course
end
Note the use of :source, which tells ActiveRecord which class the has_many is referring to. Then when you're going through and want all of a user's courses, you would do: user.saved_course_courses + user.course_completion_courses

Get attribute value from the join in a many-to-many relationship

I have a many-to-many relation between User and "Link".
The join model is called LinkAddress and besides for saving the IDs of the other two models, it has an attribute called address - information it collects at creation.
How can I access the address attribute for a certain link in a request scenario like the following: User.first.links.first.address ?
Models:
class User < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :links, through: :link_addresses
accepts_nested_attributes_for :link_addresses, allow_destroy: true
end
class LinkAddress < ActiveRecord::Base
belongs_to :user
belongs_to :link
end
class Link < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :users, through: :link_addresses
end
You could access it through User since it's a has_many ... :through relation:
User.first.link_addresses.first.address
Or, if you'd like to go through links then:
User.first.links.first.link_addresses.first.address
SQL Aliases
I had this exact question: Rails Scoping For has_many :through To Access Extra Data
Here's the answer I got:
#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy
This uses SQL Aliases which I found at this RailsCast (at around 6:40 in). It allows us to call #user.image.caption (even though .caption is in the join model)
Your Code
For your query, I'd use this:
class User < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :links, -> { select("#{Link.table_name}.*, #{LinkAddress.table_name}.address AS address") }, through: :link_addresses
accepts_nested_attributes_for :link_addresses, allow_destroy: true
end
This will allow you to write #user.links.first.address, and gracefully handles an absence of the address record

Rails: Polymorphic has_many: through workarounds

My app allows users to follow several different model types, including other users, organizations, products, etc. Each of these models has a has_many: :histories association. My goal is to compile all the :histories of the resources the current_user is following.
My models look like this:
class Follow < ActiveRecord::Base
belongs_to :user
belongs_to :followable, polymorphic: true
end
class History < ActiveRecord::Base
belongs_to :historical, polymorphic: true
end
class User < ActiveRecord::Base
has_many :follows
has_many :followed_resources, through: :follows, source: :followable
has_many :followed_histories, through: :followed_resources, source: :histories
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
class Product < ActiveRecord::Base
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
class Organization < ActiveRecord::Base
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
etc
My goal is to get the histories from all the current_user's followed resources like so:
#histories = current_user.followed_histories
But unfortunately, Rails does not allow us to traverse polymorphic association in a has_many: through relationship. Instead, it insists that we specify only a single association using the source_type option. For instance
has_many :followed_products, through: :follows, source: :followable, source_type: :product
Unfortunately, this approach won't work in this case, unless there's some way to recombine all the associations afterwards. For instance, if there was some way to do this:
has_many :followed_histories, through: {:followed_products, :followed_organizations, :followed_users}
Or perhaps there's yet another approach I'm not considering. I'm open to any suggestions, so long as the end result is a single array containing the combined histories of the followed resources.

Resources