double belongs_to with validation on rails - ruby-on-rails

I have the following models
class School
has_many :classrooms
has_many :communications
end
class Classroom
belongs_to :school
end
class Communication
belongs_to :school
end
At the moment I can have a school_id in communication, however due to business logic I realized that I might have to index the communication also with a classroom, making the models to be like this:
class School
has_many :classrooms
has_many :communications
end
class Classroom
belongs_to :school
has_many :communications
end
class Communication
belongs_to :school
belongs_to :classroom, optional: true
end
What I want is that a communication should always belong to a school, but if it belongs to a classroom i want to make sure that the classroom also belongs to the same school
How can I write a validation for this case?

For this case I ended up creating a custom validator:
class ClassroomValidator < ActiveModel::Validator
def validate(record)
if record.classroom.school.id != record.school.id
record.errors.add :classroom, message: "Invalid relation between classroom and school"
end
end
end
class Communication < ApplicationRecord
belongs_to :school
belongs_to :classroom, optional: true
validates_with ClassroomValidator, if: -> { self.classroom != nil }
end

Related

Rails 5 model custom validation with relation has many

In my Rails 5.2 app, I have the following Model relations.
A Job has many Contracts and has many Expertises. Resulting in the following Ruby on Rails models.
job.rb
class Job < ApplicationRecord
has_many :attached_expertises, as: :expertisable, dependent: :destroy
has_many :expertises, through: :attached_expertises
has_many :attached_contracts, as: :contractable, dependent: :destroy
has_many :contracts, through: :attached_contracts
end
contract.rb
class Contract < ApplicationRecord
has_many :attached_contracts, dependent: :destroy
end
attached_contract.rb
class AttachedContract < ApplicationRecord
belongs_to :contract
belongs_to :contractable, polymorphic: true
end
expertise.rb
class Expertise < ApplicationRecord
has_many :attached_expertises
end
attached_expertise.rb
class AttachedExpertise < ApplicationRecord
belongs_to :expertise
belongs_to :expertisable, polymorphic: true
end
I need to perform a validation in Job depending in the values of the associated models.
I have 3 types of Contracts and 3 types of Expertises as well.
Lets say contracts can be P, C or E, and Expertises J, L, S.
When creating or updating a Job if Contract of type E is selected, Expertise must be of type J, otherwise if should raise an error.
I have tried this creating custom validation ActiveModel::EachValidator. But was not able to get it to work. Neither with a custom method in the model it self.
Whats is the best way to achieve this?
And where should I place the validator file? In app/models/concerns/?
You can use validates_with and write a custom method to do the checking
class Person < ActiveRecord::Base
validates_with GoodnessValidator
end
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
record.errors[:base] << "This person is evil"
end
end
end

Validate relationship using associated table

I have the following models:
class Property < ActiveRecord::Base
belongs_to :property_type
has_many :variant_properties
has_many :variants, through: :variant_properties
end
class PropertyType < ActiveRecord::Base
has_many :properties
end
class Variant < ActiveRecord::Base
has_many :variant_properties
has_many :properties, through: :variant_properties
end
class VariantProperty < ActiveRecord::Base
belongs_to :property
belongs_to :variant
validates_uniqueness_of :property, scope: :property_type
end
What I am trying to validate is that two Properties for the same Variant should never belong to the same Property_Type.
Is there any way to perform this validation in a Rails way?
EDIT:
Finally solved using a custom validator, as suggested by #qaisar-nadeem. A redundant column would be also ok, but I would consider it an optimization more than a solution.
class Property < ActiveRecord::Base
(...)
validate :property_type_uniqueness
private
def property_type_uniqueness
unless property_type_unique?
msg = 'You cannot have multiple property variants with same property type'
errors.add(:property_id, msg)
end
end
def property_type_unique?
VariantProperty
.where(variant: variant)
.select { |vp| vp.property.property_type == property.property_type }
.empty?
end
end
Validates scope cannot access joined table so you will need to have custom validation.
So there are two options.
Option 1 : Use a custom validator and have SQL check if there is any Property Variant with same Property Type.
Custom Validations guide can be found on http://guides.rubyonrails.org/active_record_validations.html#custom-validators
Option 2 : Add a redundant column property_type_id in variant_properties model and then add validates_uniqueness_of :property, scope: :property_type as you already have done
UPDATE
Here is the custom validator
class VariantProperty < ActiveRecord::Base
belongs_to :property
belongs_to :variant
validate :check_property_type_uniqueness
def check_property_type_uniqueness
errors.add(:property_id, "You cannot have multiple property variants with same property type") if VariantProperty.joins(:property).where(:property_id=>self.property_id,:variant_id=>self.variant_id,:properties=>{:property_type_id=>self.property.property_type_id}).count > 0
end
end
your should add relation with PropertyType to VariantProperty
class VariantProperty < ActiveRecord::Base
belongs_to :property
belongs_to :variant
has_one :property_type, through: :property
validates :property_type, uniqueness: { scope: :variant_id }
end

Dynamic has_many class_name using polymorphic reference

I am trying to associate a polymorphic model (in this case Product) to a dynamic class name (either StoreOnePurchase or StoreTwoPurchase) based on the store_type polymorphic reference column on the products table.
class Product < ActiveRecord::Base
belongs_to :store, polymorphic: true
has_many :purchases, class_name: (StoreOnePurchase|StoreTwoPurchase)
end
class StoreOne < ActiveRecord::Base
has_many :products, as: :store
has_many :purchases, through: :products
end
class StoreOnePurchase < ActiveRecord::Base
belongs_to :product
end
class StoreTwo < ActiveRecord::Base
has_many :products, as: :store
has_many :purchases, through: :products
end
class StoreTwoPurchase < ActiveRecord::Base
belongs_to :product
end
StoreOnePurchase and StoreTwoPurchase have to be separate models because they contain very different table structure, as does StoreOne and StoreTwo.
I am aware that introducing a HABTM relationship could solve this like this:
class ProductPurchase < ActiveRecord::Base
belongs_to :product
belongs_to :purchase, polymorphic: true
end
class Product < ActiveRecord::Base
belongs_to :store, polymorphic: true
has_many :product_purchases
end
class StoreOnePurchase < ActiveRecord::Base
has_one :product_purchase, as: :purchase
delegate :product, to: :product_purchase
end
However I am interested to see if it is possible without an extra table?
Very interesting question. But, unfortunately, it is impossible without an extra table, because there is no polymorphic has_many association. Rails won't be able to determine type of the Product.purchases (has_many) dynamically the same way it does it for Product.store (belongs_to). Because there's no purchases_type column in Product and no support of any dynamically-resolved association types in has_many. You can do some trick like the following:
class Product < ActiveRecord::Base
class DynamicStoreClass
def to_s
#return 'StoreOnePurchase' or 'StoreTwoPurchase'
end
end
belongs_to :store, polymorphic: true
has_many :purchases, class_name: DynamicStoreClass
end
It will not throw an error, but it is useless, since it will call DynamicStoreClass.to_s only once, before instantiating the products.
You can also override ActiveRecord::Associations::association to support polymorphic types in your class, but it is reinventing the Rails.
I would rather change the database schema.

Rails order by associated data

Is it possible to order the results of school.classrooms by the teacher's name? I want to do this directly in the association, and not a separate call.
class School < ActiveRecord::Base
has_many :classrooms
end
class Classroom < ActiveRecord::Base
belongs_to :school
belongs_to :teacher
end
class Teacher < ActiveRecord::Base
has_one :classroom
end
This should work if you are using rails 3.x
school.classrooms.includes(:teacher).order("teachers.name")

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