Getting Association through self-inheriting STI model - ruby-on-rails

In my Rails app I have Users and Forms.
class User < ActiveRecord::Base
has_many :admin_roles
#desired association below
#has_many :forms, through: :admin_roles
end
class Form < ActiveRecord::Base
has_one :department
end
The Users need to administrate the Forms through any level of an Organization.
class AdminRole < ActiveRecord::Base
belongs_to :organization
belongs_to :user
end
If assigned to a non-department organization the forms they have control over should come through the child departments.
The Forms are assigned to a departmental level only.
My model for the Organization is an STI model of 3 levels: market>subdomain>department
class Organization < ActiveRecord::Base
self.inheritance_column = :org_level
has_many :admin_roles
end
class Department < Organization
belongs_to :sub_domain, primary_key: :id, foreign_key: :parent_id
has_many :forms
end
class SubDomain < Organization
belongs_to :market, primary_key: :id, foreign_key: :parent_id
has_many :departments
end
class Market < Organization
has_many :sub_domains
end
The desired capability is to do user.forms and get all the associated forms back.
For example: Given there was a hierarchy of FooMarket>BarDomain>LoremDepartment
and a Form associated to LoremDepartment.
If a User is then tied to any of those 3 Organizations through the AdminRole it would allow for the return of the LoremDepartment Form.

Do you have to necessarily do it with associations ? u can always define an instance method in user model and back track it to forms.
But before that, just a reminder, you have to mention the foreign key in both the models for the association to work both ways.
class User < ActiveRecord::Base
attr_accessible :name
has_many :admin_roles
has_many :organizations, :through => :admin_roles
def forms
organizations.map(&:forms).flatten.uniq
end
end
class Department < Organization
belongs_to :sub_domain, primary_key: :id, foreign_key: :parent_id
has_many :forms, :foreign_key => :organization_id
end
class SubDomain < Organization
belongs_to :market, primary_key: :id, foreign_key: :parent_id
has_many :departments, foreign_key: :parent_id
def forms
departments.map(&:forms).flatten
end
end
class Market < Organization
has_many :sub_domains, foreign_key: :parent_id
def forms
sub_domains.map(&:forms).flatten
end
end
I tested this and it does work. But kinda round about.

Related

How to create a group of users (roomates) within one product (property) in Rails

I have a question on a platform I'm developing in Ruby on Rails 5.2.
I have an Owner model which is the owner of properties/property. The owner will post a property so that users (in this case roomates) can share the same property/house/department, etc.
I have Owners and I have Users (both tables are created using devise):
Owner.rb:
class Owner < ApplicationRecord
has_many :properties
end
User.rb:
class User < ApplicationRecord
#Theres nothing here (yet)
end
This is where the magic happens. Property.rb:
class Property < ApplicationRecord
belongs_to :owner
has_many :amenities
has_many :services
accepts_nested_attributes_for :amenities
accepts_nested_attributes_for :services
mount_uploaders :pictures, PropertypictureUploader
validates :amenities, :services, presence: true
scope :latest, -> { order created_at: :desc }
end
How can multiple users share a property? I'm aware that it will have a many-to-many association but I'm a bit confused how to connect these relationships so when the owner posts a property it will display something like:
Property available for: 3 users
And then begin to limit users until it completes the amount of users available.
This sounds like your average many to many assocation:
class User < ApplicationRecord
has_many :tenancies, foreign_key: :tenant_id
has_many :properties, through: :tenancies
end
class Tenancy < ApplicationRecord
belongs_to :tenant, class_name: 'User'
belongs_to :property
end
class Property < ApplicationRecord
has_many :tenancies
has_many :tenants, through: :tenancies
def availablity
# or whatever attribute you have that defines the maximum number
max_tenants - tenancies.count
end
end
You can restrict the number of tenants with a custom validation.
You can use a join table, called users_properties. This table will have a property_id and user_id. You'll then have the following in your properties model:
has_many :users_properties
has_many :users, through: :users_properties
Read more about it here https://guides.rubyonrails.org/association_basics.html

Polymorphic has-many-through relationships in Rails

I am trying to set up a polymorphic has-many-through relationship with ActiveRecord. Here's the end goal:
Users can belong to many organizations and many teams
Organizations have many users and many teams
Teams have many users and belong to an organization
I am trying to use has-many-through instead of has-and-belongs-to-many, since I need to associate some information along with the relationships (like user role in the organization or team), so I made a join table Membership.
How would I implement this?
I would design the schema like this:
Organization has many Team
Team has many TeamMember
User has many TeamMember
TeamMember belongs to User and Team
The models will be:
organization.rb
class Organization < ActiveRecord::Base
has_many :teams
has_many :team_members, through: :teams
has_many :users, through: :team_members
end
team.rb
class Team < ActiveRecord::Base
belongs_to :organization # fk: organization_id
has_many :team_members
has_many :users, through: :team_members
end
user.rb
class User < ActiveRecord::Base
has_many :team_members
has_many :teams, through: :team_members
has_many :organizations, though: :teams
end
team_member.rb
class TeamMember < ActiveRecord::Base
belongs_to :team # fk: team_id
belongs_to :user # fk: user_id
attr_accessible :role # role in team
end
So, compare with your requirements:
Users can belong to many organizations and many teams
=> Okay
Organizations have many users and many teams
=> Okay
Teams have many users and belong to an organization
=> Okay
Btw, we don't use any polymorphic here, and TeamMember stands for Membership in your early idea!
For polymorphic association,
class User
has_many :memberships
end
class Team
belongs_to :organization
has_many :memberships, :as => :membershipable #you decide the name
end
class Organization
has_many :memberships, :as => :membershipable
has_many :teams
end
class Membership
belongs_to :user
belongs_to :membershipable, polymorphic: true
end
Note that User is indirectly associated to Team and Organization, and that every call has to go through Membership.
In my projects, I use a Relationship class (in a gem I've named ActsAsRelatingTo) as the join model. It looks something like this:
# == Schema Information
#
# Table name: acts_as_relating_to_relationships
#
# id :integer not null, primary key
# owner_id :integer
# owner_type :string
# in_relation_to_id :integer
# in_relation_to_type :string
# created_at :datetime not null
# updated_at :datetime not null
#
module ActsAsRelatingTo
class Relationship < ActiveRecord::Base
validates :owner_id, presence: true
validates :owner_type, presence: true
validates :in_relation_to_id, presence: true
validates :in_relation_to_type, presence: true
belongs_to :owner, polymorphic: true
belongs_to :in_relation_to, polymorphic: true
end
end
So, in your User model, you would say something like:
class User < ActiveRecord::Base
has_many :owned_relationships,
as: :owner,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
has_many :organizations_i_relate_to,
through: :owned_relationships,
source: :in_relation_to,
source_type: "Organization"
...
end
I believe you may be able to leave the source_type argument off since the joined class (Organization) can be inferred from :organizations. Often, I'm joining models where the class name cannot be inferred from the relationship name, in which case I include the source_type argument.
With this, you can say user.organizations_i_relate_to. You can do the same set up for a relationship between any set of classes.
You could also say in your Organization class:
class Organization < ActiveRecord::Base
has_many :referencing_relationships,
as: :in_relation_to,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
has_many :users_that_relate_to_me,
through: :referencing_relationships,
source: :owner,
source_type: "User"
So that you could say organization.users_that_relate_to_me.
I got tired of having to do all the set up, so in my gem I created an acts_as_relating_to method so I can do something like:
class User < ActiveRecord::Base
acts_as_relating_to :organizations, :teams
...
end
and
class Organization < ActiveRecord::Base
acts_as_relating_to :users, :organizations
...
end
and
class Team < ActiveRecord::Base
acts_as_relating_to :organizations, :users
...
end
and all the polymorphic associations and methods get set up for me "automatically".
Sorry for the long answer. Hope you find something useful in it.

Ownership conditional in a belongs_to association

I have two models: Users and Posts. The way I have things setup, a post belongs to an owner (i.e. user) and also has many participants (i.e. users). In my User model I'd like to ensure that an owner never belongs to a post. I've done this in the front-end but found more code than need-be.
This led me to believe that using conditions would be an ideal solution. I've seen SQL conditions used in this manner but didn't know exactly what the best way to get this done for an ownership scenario. Suggestions?
class User < ActiveRecord::Base
has_many :posts
# belongs_to :posts, conditions: ...
end
class Post
has_many :participants, class_name: "User", foreign_key: "user_id"
belongs_to :owner, class_name: "User", foreign_key: "user_id"
end
To acheive this, I think you need a third model. If you set things up as follows it should work:
User model:
class User < ActiveRecord::Base
has_many :posts # This is the other side of your owner association
has_many :user_posts # This is your link table for participants
has_many :participations, through: :user_posts, source: :user # These are the posts the user is a participant in
end
Post model:
class Post < ActiveRecord::Base
has_many :user_posts, ->(p) { where.not(user_id: p.user_id) } # Here is your condition for the participants
has_many :participants, through: :user_posts, source: :user
belongs_to :owner, class_name: "User", foreign_key: "user_id"
end
UserPost model:
class UserPost < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
As #Oxynum's answer makes clear, you should also think about putting a validation in the UserPost model to prevent the participant from being saved if he is also the owner:
validate :participant_cannot_be_owner
def participant_cannot_be_owner
if user == post.try(:owner)
errors.add(:user_id, "can't be the owner of the post")
end
end
First, there is probably an error in your associations, cause it seems like you need a join table for the participants relationship.
You should probably use a http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
has_many through association.
Something like this :
class User < ActiveRecord::Base
has_one :owned_post, class_name: "Post", foreign_key: :owner_id
has_many :participations
has_many :posts, through: :participations
end
class Participation < ActiveRecord::Base
belongs_to :post
belongs_to :participant, class_name: "User"
end
class Post < ActiveRecord::Base
belongs_to :owner, class_name: "User"
has_many :participants, through: :participations
end
When you have this model, you can use a validation on the participation model to prevent an owner to be a participant. By using a custom validation : http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations
class Participation < ActiveRecord::Base
belongs_to :post
belongs_to :participant, class_name: "User"
validate :participant_is_not_the_owner
def participant_is_not_the_owner
if participant == post.owner
errors.add(:participant, "can't be the owner")
end
end
end

Rails model associations, has_many :through and has_one with the same model

I have two models: User and State. The state model has records for each of the 50 states in the United States.
I would like each User to have two attributes: one "home" state, and many states to "visit".
I know I have to set up some sort of model associations to achieve this, but not sure what the best approach is.
Here's what I have so far, but I know there must be something wrong with have has_many and has_one association to the same model.
#user.rb
class User < ActiveRecord::Base
has_many :visits
has_many :states, :through => :visits
has_one :state
end
#visit.rb
class Visit < ActiveRecord::Base
belongs_to :user
belongs_to :state
end
#state.rb
class State < ActiveRecord::Base
has many :visits
has many :users, :through => :visits
belongs_to :user
end
Any suggestions?
In my opinion what you already have is almost right, except you would store the home state foreign key on the user like thus:
# user.rb
class User < ActiveRecord::Base
belongs_to :state
has_many :visits
has_many :states, through: visits
end
# visit.rb
class Visit < ActiveRecord::Base
belongs_to :user
belongs_to :state
end
# state.rb
class State < ActiveRecord::Base
has_many :visits
has_many :users, through: :visits
end
You would then access the home state like thus:
u = User.first
u.state
And the visited states, like thus:
u = User.first
u.states
For programming clarity, you can rename your relations:
# user.rb
class User < ActiveRecord::Base
belongs_to :home_state, class_name: "State"
has_many :visits
has_many :visited_states, class_name: "State", through: visits
end
# state.rb
class State < ActiveRecord::Base
has_many :residents, class_name: "User"
has_many :visits
has_many :visitors, class_name: "User", through: :visits
end
Your domain model would make more sense:
u = User.first
u.home_state
u.visited_states
s = State.first
s.residents
s.visitors
I expect you'll probably want to store additional information about the visit, so keeping the HMT join table for the Visit model will allow you to do this, rather than going with a HABTM relation. You could then add attributes to the visit:
# xxxxxxxxxxxxxxxx_create_visits.rb
class CreateVisits < ActiveRecord::Migration
def change
create_table :visits do |t|
t.text :agenda
t.datetime commenced_at
t.datetime concluded_at
t.references :state
t.references :user
end
end
end
You can't have a has_many and has_one relationship on a single model, in this case state. One solution is to:
create a static model of states, they do not need to be a database model, they could be a static variable on the state model: US_STATES = {'1' => 'AK', '2' => 'AL', etc} or you could use fixtures to load a table of states into the database (more complicated because you need to use a rake task or the db:seed task to load the fixtures into the db, but nice because you can use active record to manage the model).
then you can provide a home_state_id on the user model that defines the home_state and the visits are simply a join between user_id and the state_id.
I would like each User to have two attributes: one "home" state, and many states to "visit".
In your models, a state may only be home to one user (belongs_to).
The correct semantics would be
class User < AR::Base
belongs_to :home_state, :class_name => "State", :foreign_key => "home_state_id", :inverse_of => :users_living
has_and_belongs_to_many :visited_states, :through => :visits
# ...
end
class State < AR::Base
has_many :users_living, :class_name => "User", :inverse_of => :home_state
# ...
end

Can a 3-way relationship be modeled this way in Rails?

A User can have many roles, but only one role per Brand.
Class User < AR::Base
has_and_belongs_to_many :roles, :join_table => "user_brand_roles"
has_and_belongs_to_many :brands, :join_table => "user_brand_roles"
end
The problem with this setup is, how do I check the brand and the role at the same time?
Or would I better off with a BrandRole model where different roles can be set up for each Brand, and then be able to assign a user to a BrandRole?
Class User < AR::Base
has_many :user_brand_roles
has_many :brand_roles, :through => :user_brand_roles
end
Class BrandRole < AR::Base
belongs_to :brand
belongs_to :role
end
Class UserBrandRole < AR::Base
belongs_to :brand_role
belongs_to :user
end
This way I could do a find on the brand for the user:
br = current_user.brand_roles.where(:brand_id => #brand.id).includes(:brand_role)
if br.blank? or br.role != ADMIN
# reject access, redirect
end
This is a new application and I'm trying to learn from past mistakes and stick to the Rails Way. Am I making any bad assumptions or design decisions here?
Assuming Roles,Brands are reference tables. You can have a single association table Responsibilities with columns user_id, role_id, brand_id.
Then you can define
Class User < AR::Base
has_many : responsibilities
has_many :roles, :through => responsibilities
has_many :brands,:through => responsibilities
end
Class Responsibility < AR::Base
belongs_to :user
has_one :role
has_one :brand
end
The you can define
Class User < AR::Base
def has_access?(brand)
responsibility = responsibilities.where(:brand => brand)
responsibility and responsibility.role == ADMIN
end
end
[Not sure if Responsibility is the term used in your domain, but use a domain term instead of calling it as user_brand_role]
This is a conceptual thing. If BrandRole is an entity for your application, then your approach should work. If BrandRole is not an entity by itself in your app, then maybe you can create a UserBrandRole model:
class User < AR::Base
has_many :user_brand_roles
end
class Brand < AR::Base
has_many :user_brand_roles
end
class Role < AR::Base
has_many :user_brand_roles
end
class UserBrandRole < AR::Base
belongs_to :user
belongs_to :brand
belongs_to :role
validates_uniqueness_of :role_id, :scope => [:user_id, :brand_id]
end

Resources