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
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?
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.
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
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"
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.