Many to many relationship , rails - ruby-on-rails

I have a many-to-many relation between users and the channels they subscribe to. But when I look at my model dependency between user and user channels or channels and users channel instead there is a direct connection between users and channels. how do i put Users channels between to two?
User Model
class User < ActiveRecord::Base
acts_as_authentic
ROLES = %w[admin moderator subscriber]
has_and_belongs_to_many :channels
has_many :channel_mods
named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} }
def roles
ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? }
end
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
end
def role_symbols
role.map do |role|
role.name.underscore.to_sym
end
end
end
Channel Model
class Channel < ActiveRecord::Base
acts_as_taggable
acts_as_taggable_on :tags
has_many :messages
has_many :channel_mods
has_and_belongs_to_many :users
end
UsersChannel Model
class UsersChannels < ActiveRecord::Base
end

See the has_many :through documentation on the Rails guides which guides you through configuring a has_many relationship with an intervening model.

The HABTM relationship generates the UsersChannels auto-magically - if you want to access the model for the link table (add some more attributes to it for example - time_channel_watched or whatever), you'll have to change the models (and explicitly define and migrate a UsersChannel model with the attributes id:primary_key, user_id:integer, channel_id:integer) to :
class Channel < ActiveRecord::Base
has_many :users_channels, :dependent => :destroy
has_many :users, :through => :users_channels
end
class User < ActiveRecord::Base
has_many :users_channels, :dependent => :destroy
has_many :channels, :through => :users_channels
end
class UsersChannels < ActiveRecord::Base
belongs_to :user
belongs_to :channel
end
Note: since you're defining you're own link model you don't have to stay with the HABTM defined table name of UsersChannels - you could change the model name to something like "Watches". All of the above is pretty much in the Rails guide that has been mentioned.

Related

One relationship with multiple models [interesting rails relations concept]

I have this app where the model User can have multiple "channels". The channels part of the app has to be easily extendable each with its own model.
I started creating a Channel model with a belongs_to User relationship with the reverse of User has_many Channel.
I was thinking about having an API similar to this:
user = User.create(name: 'John')
user.channels.create(name: 'google', account_id: '1234') # should create a GoogleChannel::Google Model
user.channels.create(name: 'facebook', account_name: 'qwerty') # should create a FacebookChannel::Facebook Model
I am playing around with having a Channel model and each channel model having a dedicated model for each of the channels (google, facebook, etc.) with a has_one relationship to that model.
Update:
I'm using mongoid with rails
I'm not sure if this works. It uses STI.
First approach: Single Table Inheritance
class User << ApplicationRecord
has_many :channels
delegate :google_channels, :facebook_channels, :twitter_channels, to: :channels
end
class Channel << ApplicationRecord
belongs_to :user
self.inheritance_column = :brand
scope :google_channels, -> { where(brand: 'Google') }
scope :facebook_channels, -> { where(brand: 'Facebook') }
scope :twitter_channels, -> { where(brand: 'Twitter') }
def self.brands
%w(Google Facebook Twitter)
end
end
class GoogleChannel << Channel; end
class FacebookChannel << Channel; end
class TwitterChannel << Channel; end
I think you can:
current_user.channels << GoogleChannel.new(name: "First Google Channel")
current_user.channels << Facebook.new(name: "Facebook")
current_user.channels << Twitter.new(name: "Tweety")
current_user.channels << GoogleChannel.new(name: "Second Google Channel")
googs = current_user.google_channels
all = current_user.channels
# etc.
All channels share the same table. If you need different attributes for each different brand, this would not be the best option.
Second approach: Polymorphic models
If you need different tables for each model (brand), you can use a polymorphic approach (not tested):
class User << ApplicationRecord
has_many :channels
has_many :google_channels, through: :channels, source: :branded_channel, source_type: 'GoogleChannel'
has_many :facebook_channels, through: :channels, source: :branded_channel, source_type: 'FacebookChannel'
end
class Channel << ApplicationRecord
belongs_to :user
belongs_to :branded_channel, polymorphic: true
end
#This channel has its own table, and can have more attributes than Channel
class GoogleChannel << ApplicationRecord
has_one :channel, as: :branded_channel
end
#This channel has its own table, and can have more attributes than Channel
class FacebookChannel << ApplicationRecord
has_one :channel, as: :branded_channel
end
goog = GoogleChannel.create(all_google_channel_params)
face = GoogleChannel.create(all_facebook_channel_params)
current_user.channels << Channel.new(branded_channel: goog)
current_user.channels << Channel.new(branded_channel: face)
I assume you want to create STI where User has many channels, so in Rails 5 you can try this:
class User < ApplicationRecord
has_many :user_channels
has_many :channels, through: :user_channels
has_many :facebook_channels, class_name: 'FacebookChannel', through: :user_channels
has_many :google_channels, class_name: 'GoogleChannel', through: :user_channels
end
class UserChannel < ApplicationRecord
# user_id
# channel_id
belongs_to :user
belongs_to :channel
belongs_to :facebook_channel, class_name: 'FacebookChannel', foreign_key: :channel_id, optional: true
belongs_to :google_channel, class_name: 'GoogleChannel', foreign_key: :channel_id, optional: true
end
class Channel < ApplicationRecord
# You need to have "type" column to be created for storing different chanels
has_many :user_channels
has_many :users, through: :user_channels
end
class FacebookChannel < Channel
has_many :user_channels
has_many :users, through: :user_channels
end
class GoogleChannel < Channel
has_many :user_channels
has_many :users, through: :user_channels
end
Now you can do current_user.facebook_channels.create(name: "My Facebook Channel") and get all FacebookChannel with current_user.facebook_channels
Basically it works like regular has_many through relationship with extra feature of STI - you store sub-model name in type column of Channel model.
Update
I'm sorry, I didn't know my suggestion is not working with MongoDB. Maybe you can simply create channel_type column in your channels table, have simple channel belongs_to user relationship and then do:
current_user.channels.create(name: "My Channel Name", channel_type: "facebook")
current_user.channels.where(channel_type: "facebook")
You could do simply current_user.channels which gives you all channels for user and then if you need you can do what ever you need with each record according to channel_type value.
You can even create some scopes in Channel model:
class Channel < ApplicationRecord
scope :facebook_channels, -> { where(channel_type: "facebook") }
end
And then you can do your desired current_user.channels.facebook_channels
channel_type column could be string or integer if you do it with enums.
In addition if you create visit column (e.g. "boolean"), you can do current_user.channels.where(visit: true) or create scope :visit_true, -> { where(visit: true) } and do something like current_user.channels.visit_true
What do you say?

Rails Factory Girl: How to setup has_many/through association with other model being created from nested attributes

I've been struggling with setting up a has_many/through relationship when one model object is being created through nested attributes using Factory Girl.
class CommercialInvoice < ActiveRecord::Base
has_many :shipment_commercial_invoices, :dependent => :destroy
has_many :shipments, :through => :shipment_commercial_invoices
end
class Shipment < ActiveRecord::Base
has_many :shipment_commercial_invoices, :dependent => :destroy
has_many :commercial_invoices, :through => :shipment_commercial_invoices
end
class ShipmentCommercialInvoices < ActiveRecord::Base
attr_accessible :job_id, :detail_id
belongs_to :shipment
belongs_to :commercial_invoice
end
The Shipment model is created using nested attributes inside another model supplier_invoice
class SupplierInvoice < ActiveRecord::Base
has_many :shipments, :dependent => :destroy
accepts_nested_attributes_for :shipments, :allow_destroy => true
end
class Shipment < ActiveRecord::Base
belongs_to :supplier_invoice
end
My Factories
factory :supplier_invoice do do
shipments_attributes do
shipments_attributes = []
2.times do # 2 shipments per supplier invoice
shipments_attributes << attributes_with_foreign_keys(:shipment)
end
shipments_attributes
end
end
factory :commercial_invoice do
after(:create) do |commercial_invoice, evaluator|
commercial_invoice.shipments << FactoryGirl.create(:supplier_invoice).shipments
end
end
factory :shipments_commercial_invoice do
shipment
commercial_invoice
comm_invoiced true # A boolean attribute that i want to be true
end
Now the problem in whenever i create a new commercial invoice, the associated shipment_commercial_invoice's attribute comm_invoiced is always false. I am not able to figure out what am i doing wrong. Is there any problem with how i defined factories? Thanks in advance.

Access parent in scope

I have a rails 3.2 application with an easy hierarchy: A user has many clients, a client has many invoices.
I want the users to only see their own clients and invoices using scopes, by doing Client.by_user(current_user) and Invoice.by_user(current_user). For clients, I have this, which works fine:
scope :by_user, lambda { |user| where(user_id: user.id) }
However, if I try the same for invoices
scope :by_user, lambda { |user| where(client.user_id => user.id) }
it fails, telling me undefined local variable or method 'client'.
What am I doing wrong? I don't want to add user_ids to the invoices.
As #gregates said in comments, better for you to define associations for User, Client & Invoice models and then use user.clients, user.invoices, invoice.user etc.:
class User < ActiveRecord::Base
has_many :clients
has_many :invoices, through: :clients
end
class Client < ActiveRecord::Base
belongs_to :user
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :client
has_one :user, through: :client
end
But if you prefer idea with scope, you should join clients table to invoices in your scope:
class Invoice < ActiveRecord::Base
...
scope :by_user, lambda { |user| joins(:client).where("clients.user_id = ?", user.id) }
...
end

Naming error in controller

I get this error when trying to create a record in my joins table
NameError in
SubscriptionsController#new
uninitialized constant
Channel::ChannelsUser
Subscriptions Controller
class SubscriptionsController < ApplicationController
helper_method :current_user_session, :current_user
filter_parameter_logging :password, :password_confirmation
def new
#channel = Channel.find(params[:channel_id])
#user = current_user
#channel.subscribers << #user
#channel.save
flash[:notice] = "You have subscribed to: " +#channel.name
redirect_to #channel
end
end
end
User Model
class User < ActiveRecord::Base
acts_as_authentic
ROLES = %w[admin moderator subscriber]
#Each user can subscribe to many channels
has_many :channels_users
has_many :subscriptions, :class_name => "Channel", :through => :channels_users
#Each user who is a moderator can moderate many channels
has_many :channel_mods
has_many :channels, :through => :channel_mods
#Each user can receive many messages
has_many :messages_users , :dependent => :destroy
has_many :reciepts , :class_name => "User", :through => :messages_users
#Filter users by role(s)
named_scope :with_role, lambda { |role| {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0 "} }
def roles
ROLES.reject { |r| ((roles_mask || 0) & 2**ROLES.index(r)).zero? }
end
def roles=(roles)
self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
end
def role_symbols
role.map do |role|
role.name.underscore.to_sym
end
end
end
Channel Model
class Channel < ActiveRecord::Base
#Each channel owns many or no messages
has_many :messages
#Each channel is own by one moderator
has_many :channel_mods
has_many :moderators, :class_name =>'User', :through =>:channel_mod
#Each channel can have and belong to many or no users
has_many :channels_users
has_many :subscribers, :class_name => 'Users' , :through => :channels_users
end
ChannelsUsers model
class ChannelsUsers < ActiveRecord::Base
belongs_to :user
belongs_to :channel
end
This would read much nicer if you change the model to ChannelUser. Here are the corresponding relationships:
class Channel < ActiveRecord::Base
has_many :channel_users
has_many :users, :through => :channel_users
end
class User < ActiveRecord::Base
has_many :channel_users
has_many :channels, :through => :channel_users
end
class ChannelUser < ActiveRecord::Base
belongs_to :channel
belongs_to :user
end
Your join table would then be called channel_users. I think you named it channels_users initially because that's the setup for a has_and_belongs_to_many join table. But since you're using has_many :through, you're free to name the table as you like.
I wrote a blog article earlier this year that walks through all the options in detail:
Basic Many-to-Many Associations in Rails
I hope this helps!
Your channel user class name is a plural. It is supposed to be singular.
So either you can change to this:
class ChannelsUser < ActiveRecord::Base
belongs_to :user
belongs_to :channel
end
or change this line in User and Channel model:
has_many :channels_users
to
has_many :channels_users, :class_name => 'ChannelsUsers'
Rails will use the methods like String#classify and String#underscore to detect classes and relationships.
If you want to play around with the names, in the console try out various combinations:
>> "channels_users".classify
=> "ChannelsUser"
>> "ChannelsUser".underscore
=> "channels_user"

has_many :through default values

I have a need to design a system to track users memberships to groups with varying roles (currently three).
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Role < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :group
end
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
end
Ideally what I want is to simply set
#group.users << #user
and have the membership have the correct role. I can use :conditions to select data that has been manually inserted as such :
:conditions => ["memberships.role_id= ? ", Grouprole.find_by_name('user')]
But when creating the membership to the group the role_id is not being set.
Is there a way to do this as at present I have a somewhat repetitive piece of code for each user role in my Group model.
UPDATED
It should be noted what id ideally like to achieved is something similar to
#group.admins << #user
#group.moderators << #user
This would create the membership to the group and set the membership role (role_id ) appropriately.
You can always add triggers in your Membership model to handle assignments like this as they are created. For instance:
class Membership < ActiveRecord::Base
before_save :assign_default_role
protected
def assign_default_role
self.role = Role.find_by_name('user')
end
end
This is just an adaptation of your example.

Resources