How to manage multiple roles using polymorphic association in rails? - ruby-on-rails

I am using devise for authentication and finding a way to get out of this.
Can I explore same design user having multiple roles ?. So that he can login as Teacher or Parent both?. Basically he can switch accounts like multiple roles.
class User < ActiveRecord
belongs_to :loginable, polymorphic: true
end
class Parent < ActiveRecord
has_one :user, as: :loginable
end
class Teacher < ActiveRecord
has_one :user, as: :loginable
end
for eg: loginable_type: "Parent", loginable_id: 123
I want to find a way to change above fields, if user is logging in as 'Teacher' and its ID.

You can add a polymorphic has_many relationship:
class CreateUserRoles < ActiveRecord::Migration
def change
create_table :user_roles do |t|
t.integer :role_id
t.integer :user_id
t.string :role_type # stores the class of role
t.timestamps
end
add_index :user_roles, [:role_id, :role_type]
end
end
class AddActiveRoleToUser < ActiveRecord::Migration
def change
change_table :user_roles do |t|
t.integer :active_role_id
t.timestamps
end
end
end
class User < ActiveRecord
has_many :roles, polymorphic: true
has_one :active_role, polymorphic: true
def has_role? role_name
self.roles.where(role_type: role_name).any?
end
end

Related

How to associate 2 many_to_many relationship in Rails

How do you associate a double many_to_many relationship? and also, what is it called? I know that there's no "double" many_to_many.
So have these models in rails, a User, Role, UserRole, Menu, RoleMenu.
A user can access menus depending on the roles. On console, I can do this User.first.roles.first.menus. My question is there a way to do like this User.first.menus, so it'll shorten? How do you associate User to Menu? What should I add to my models? what migration should I create?
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
end
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.timestamps null: false
end
end
end
class Role < ActiveRecord::Base
has_many :user_roles
has_many :users, through: :user_roles
has_many :role_menus
has_many :menus, through: :role_menus
end
class CreateRoles < ActiveRecord::Migration
def change
create_table :roles do |t|
t.string :name
t.timestamps null: false
end
end
end
class UserRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class CreateUserRoles < ActiveRecord::Migration
def change
create_table :user_roles do |t|
t.belongs_to :user
t.belongs_to :role
t.timestamps null: false
end
end
end
class Menu < ActiveRecord::Base
has_many :role_menus
has_many :roles, through: :role_menus
end
class CreateMenus < ActiveRecord::Migration
def change
create_table :menus do |t|
t.string :name
t.timestamps null: false
end
end
end
class RoleMenu < ActiveRecord::Base
belongs_to :role
belongs_to :menu
end
class CreateRoleMenus < ActiveRecord::Migration
def change
create_table :role_menus do |t|
t.belongs_to :role
t.belongs_to :menu
t.timestamps null: false
end
end
end
Did you mean User.first.menus instead of User.menus ? Because, latter can't be achieved as you are trying to access menus through User class (which is more of a scope implementation) and not the particular user.
For the first case, as I can see that you are already aware of the has_many, through association. We will use the same to achieve that. Following should work.
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
has_many :menus, through: :roles
end
How do you associate a double many_to_many relationship? and also, what is it called? I know that there's no "double" many_to_many.
Well, yes, there's nothing called double many to many association but it is more aptly called multiple or nested many to many relation/association. And as mentioned above, it can be achieved through has_many, through
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
has_many :menus, through: :roles
end
adding another has_many.. through should work

Callbacks - Set field on model

I am trying the following -
I have Models: Tales, Books, Keywords
class Tale < ActiveRecord::Base
has_many :tale_culture_joins
has_many :cultures, through: :tale_culture_joins
has_many :tale_purpose_joins
has_many :purposes, through: :tale_purpose_joins
has_many :tale_book_joins
has_many :books, through: :tale_book_joins
has_many :tale_keyword_joins
has_many :keywords, through: :tale_keyword_joins
class Book < ActiveRecord::Base
has_many :tale_book_joins
has_many :tales, through: :tale_book_joins
end
class TaleBookJoin < ActiveRecord::Base
belongs_to :tale
belongs_to :book
end
class Keyword < ActiveRecord::Base
has_many :tale_keyword_joins
has_many :tales, through: :tale_keyword_joins
end
class TaleKeywordJoin < ActiveRecord::Base
belongs_to :tale
belongs_to :keyword
end
These are the migrations
class CreateTales < ActiveRecord::Migration
def change
create_table :tales do |t|
t.text :name, null: false, unique: true
t.boolean :exists, default: nil
t.timestamps null: false
end
end
end
class CreateBooks < ActiveRecord::Migration
def change
create_table :books do |t|
t.text :name, null: false, unique: true
t.boolean :exists, default: nil
t.timestamps null: false
end
end
end
class CreateKeywords < ActiveRecord::Migration
def change
create_table :keywords do |t|
t.text :name, null: false, unique: true
t.boolean :exists, default: nil
t.timestamps null: false
end
end
end
What I want to happen is that everytime i delete a join between (Tale, Book) or (Tale,Keyword) through following method
tale_instance_object.book_ids = []
It should go and check if the books for whom the relations have been broken have any other tale relations. If not then set :exists in Book object instance to false.
I am able to do this through controller code.
Wondering how CallBacks or ActiveModel can be used
Join classes should only really be used when the relation is an object on its own.
Consider these cases:
doctors -> appointments <- patients
years -> days <- hours
In these cases the relation object has data of its own (appointments.time, days.weekday) and logic. Otherwise you are just wasting memory since a object has to instantiated for every relation. Use has_and_belongs_to instead.
class Tale < ActiveRecord::Base
# has_and_belongs_to_many :cultures
# has_and_belongs_to_many :purposes
has_and_belongs_to_many :books
has_and_belongs_to_many :keywords
after_destroy :update_books!
end
class Book
has_and_belongs_to_many :tales
def check_status!
self.update_attribute(status: :inactive) unless books.any
end
end
class Keyword
has_and_belongs_to_many :tales
end
Also exists is a really bad naming choice for a model field since it collides with Rails built in exists? methods. This will have unexpected consequences.
A better alternative would be to use an integer combined with an enum.
class Book
# ...
enum :status [:inactive, :active] # defaults to inactive
end
class CreateBooks < ActiveRecord::Migration
def change
create_table :books do |t|
t.text :name, null: false, unique: true
t.integer :status, default: 0
t.timestamps null: false
end
end
This will also add the methods book.inactive? and book.active?.
You could add a callback to Tale which tells Book to update when a tale is updated but this code really stinks since Tale now is responsible for maintaining state of Book.
class Tale < ActiveRecord::Base
# ...
after_destroy :update_books!
def update_books!
self.books.each { |b| b.check_status! }
end
end
class Book
has_and_belongs_to_many :tales
def check_status!
self.update_attribute(status: :inactive) unless tales.any?
end
end
A better alternative to add a callback on Books or to do it in the controller when destroying a tale:
class Book
has_and_belongs_to_many :tales
before_save :check_for_tales!, if: -> { self.tales_changed? }
def check_for_tales!
self.status = :inactive unless self.tales.any?
end
end

How add association with custom FK in Ruby on Rails?

I have two models: Users and Microposts. There is an association between one-to-many. I would like to add to Microposts field replic_to and replics_from to Users, and association with many-to-many relationships that will not only be stored in the model Microposts the author, but the user ID, which is addressed to the message. Because of already having associations between models, I think you need to specify the force field as FK for new association. I ask you to show how it can be done. Thank you in advance.
This`s sources:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.string :content
t.integer :user_id
t.timestamps
end
add_index(:microposts, [:user_id, :created_at]);
end
end
class User < ActiveRecord::Base
....
has_many(:microposts, dependent: :destroy);
....
end
class Micropost < ActiveRecord::Base
....
belongs_to :user;
....
end
Add to micropost.rb
belongs_to :replics_to, class_name: 'User'
The corresponding field should be
add_column :microposts, :replics_to_id, :integer
For user.rb
has_many :replics_from, class_name: 'User', foreign_key: 'replics_to_id'

ActiveRecord Associations

I got the following use-case:
I got three types of Users: Advertisers, Publishers and Administrators. Each user has some common properties (like name or surname) but also several unique associations. The advertiser has an association with Ad(verttisement)s and Campaigns. Each of with is another model of its own.
My question is how would I go about and model that using ActiveRecords? What would the migration code look like?
Here are the model classes:
User:
class User < ActiveRecord :: Base
require 'pbkdf2'
require 'date'
has_many :messages
attribute :name, :surname, :email, :password_hash, :password_salt
attr_accessor :password, :password_confirmation, :type
attribute :user_since, :default => lambda{ Date.today.to_s }
[...]
end
Publisher:
class Publisher < User
has_many :websites
end
Advertiser:
class Advertiser < User
has_many :campaigns
has_many :ads
end
I got the following migration file to create the User:
class AddUser < ActiveRecord::Migration
def up
create_table :users do |t|
t.string :name
t.string :surname
t.string :email
t.string :password_hash
t.string :password_salt
t.date :user_since
t.string :type
end
create_table :messages do |t|
t.belongs_to :user
t.string :account_number
t.timestamps
end
end
def down
drop_table :user
end
end
How do I modify this file in order to incorporate the aforementioned associations?
Edit: Corrected the associations to use plural form.
Polymorphic relationships is one way to solve this, while another way would be to use Single Table Inheritance (STI). Each approach has its benefits and drawbacks, and your decision would probably depend in how different the subclasses of User would tend to be. The more drastically they would differ, the more the decision would tend toward polymorphic relationships.
Using STI approach:
# a single :users table
# one table for each of the other (non-user) models
class User < ActiveRecord::Base
has_many :messages
end
class Publisher < User
has_many :websites
end
class Advertiser < User
# if :campaign supports multiple user-types (polymorphic)
has_many :campaigns, :as => :user
# otherwise
has_many :campaigns
has_many :ads
end
class Message < ActiveRecord::Base
belongs_to :user
end
class Campaign < ActiveRecord::Base
# if multiple user-types will have campaigns
belongs_to :user # referential column should be :user_id
# otherwise
belongs_to :advertiser # referential column should be :advertiser_id
end
Using Polymorphic approach:
# there should be no :users table, as User will be an abstract model class
# instead make a table for each of all the other models
class User < ActiveRecord::Base
self.abstract_class = true
has_many :messages, :as => :messageable
end
class Publisher < User
has_many :websites
end
class Advertiser < User
has_many :campaigns
has_many :ads
end
class Message < ActiveRecord::Base
belongs_to :messageable, polymorphic: true # referential columns should be :messageable_id and :messageable_type
end
class Campaign < ActiveRecord::Base
# if multiple user-types will have campaigns
belongs_to :user, polymorphic: true # referential columns should be :user_id and :user_type
# otherwise
belongs_to :advertiser # referential column should be :advertiser_id
end

STI: user has multiple child class ids.( I want the user has only one of the child class id)

I'm trying to use STI because I want to use single sign-in page for a device. I want to assign either teacher_id or student_id to a user, but it turned out that all the user has both. How can I fix this problem? Below are the models and the migration.
class User < ActiveRecord::Base
...
DEFAULT_ROLE = 'Student'
after_create :set_role
attr_accessible ..., :role
has_one :role
...
private
def set_role
self.role ||= Role.find_by_name(DEFAULT_ROLE)
end
...
end
class Student < User
has_many :bookings
end
Class Teacher < User
has_many :bookings
end
class Role < ActiveRecord::Base
validates :name, :presence => true
belongs_to :user
end
Class Booking < ActiveRecord::Base
attr_accessible :student_id, :teacher_id
belongs_to :teacher, :class_name => 'Teacher'
belongs_to :student, :class_name => 'Student'
...
class CreateBookings < ActiveRecord::Migration
def change
create_table :bookings do |t|
t.integer :student_id
t.integer :teacher_id
t.date :booking_date
t.time :booking_time
t.timestamps
end
end
end
It looks like you need to separate the "role" part of User into a separate object and then allow users to have multiple roles. Sometimes these are called "profiles" as they really refer to a way of presenting the user.
You can then use the user model as a proxy for accessing these things where you'd test for the presence of the profile:
if (user.teacher)
# ...
else
flash[:notice] = "You must be a teacher to perform this operation."
end

Resources