Rails conditional validation through one to many relationship - ruby-on-rails

I am trying to make a conditional validation of a field. Such that it only validates if another field is a specific value. The problem here is, that this other field is a one to many relation, and I can't seem to get it working.
Here is the relevant code:
class CreateInvolvedPartyTypes < ActiveRecord::Migration
def change
create_table :involved_party_types do |t|
t.string :code
t.string :name
t.timestamps null: false
end
end
end
class CreateInvolvedParties < ActiveRecord::Migration
def change
create_table :involved_parties do |t|
t.string :first_name
t.string :last_name
t.references :involved_party_type
t.timestamps null: false
end
end
end
class InvolvedParty < ActiveRecord::Base
def ipt_cd?
self.involved_party_type.code == 'I'
end
validates :first_name, presence: { message: "Please insert first name" }
validates :last_name, presence: { message: "Please insert last name" }, :if => :ipt_cd?
validates :involved_party_type, presence: { message: "Please select involved party type" }
belongs_to :involved_party_type
end
The above code fails with:
undefined method `code' for nil:NilClass
Thanks for your help

The error means that self.involved_party_type in InvolvedParty#ipt_cd? is nil. You should test the presence of involved_party_type before calling #code on it, or use #try.
def ipt_cd?
return false if involved_party.nil?
involved_party_type.code == 'I'
end
def ipt_cd?
self.involved_party_type.try(:code) == 'I'
end
Or you can avoid the problem by only invoking the validation if involved_party_type exists.
validates :last_name, presence: { message: "Please insert last name" }, if: -> { involved_party_type && ipt_cd? }

I think the issue is that you're getting confused with calling instance and class level data.
"Instance" data is populated each time a class is invoked
"Class" data is static, always appending to the class
The cosmetic difference between the two is that class data is typically called through self (EG def self.method & self.attribute), whilst instance data is called with "naked" attributes (IE without self).
You're calling the following:
def ipt_cd?
self.involved_party_type.code == 'I'
end
The problem is that you're referencing self as if it's a piece of class data. What you want is the instance equivalent:
def ipt_cd?
involved_party_type.code == 'I'
end
As the other answer states, your error is caused by a piece of data having no method for code, meaning it's nil.
The casue of this is here (solution is above -- IE remove self):
involved_party_type.code == 'I'
Thus, if you want to make sure you don't receive this error, you'll have to ensure that involved_party_type is present. This can be done by first ensuring you're referencing the instance variant of the data, followed by ensuring it's there anyway. The other answer provided the best way to achieve that.
Finally, I think your structure could be improved.
Referencing the actual data representation of an associated field is bad practice in my opinion. You're trying to create a new piece of data, and yet you're referencing an associated attribute?
Why not do the following:
#app/models/party_type.rb
class PartyType < ActiveRecord::Base
has_many :involved_parties
end
class InvolvedParty < ActiveRecord::Base
belongs_to :party_type
validates :first_name, :party_type, presence: true
validates :last_name, presence: { message: "Please insert last name" }, if: :cd?
private
def cd?
party_type == PartyType.find_by(code: "I").pluck(:id)
end
end
This will send another DB query but it removes the dependency on specific data. Your current setup is not relying on foreign keys, but on a value which may change.
Whilst this recommendation also relies on data (IE code == I), it uses it as a quantifier within ActiveRecord. That is, you're not comparing the data, but the relationship.

Related

Value Object enum not being properly validated

I have a few enums in my project that will be reused across multiple models, and a few of which will have their own internal logic, and so I've implemented them as value objects (as described here # section 5) but I can't seem to get ActiveRecord validations to work with them. The simplest example is the Person model with a Gender value object.
Migration:
# db/migrate/###_create_people.rb
class CreatePeople < ActiveRecord::Migration[5.2]
def change
create_table :people do |t|
t.string :name
t.integer :age
t.integer :gender
end
end
end
Model:
# app/models/person.rb
class Person < ApplicationRecord
validates :gender, presence: true
enum gender: Enums::Gender::GENDERS
def gender
#gender ||= Enums::Gender.new(read_attribute(:gender))
end
end
Value Object:
# app/models/enums/gender.rb
module Enums
class Gender
GENDERS = %w(female male other).freeze
def initialize(gender)
#gender = gender
end
def eql?(other)
to_s.eql?(other.to_s)
end
def to_s
#gender.to_s
end
end
end
The only problem is that despite the model being set to validate the presence of the gender attribute, it allows a Person to be saved with a gender of nil. I'm not sure why that is, so I'm not sure where to start trying to fix the problem.
So I figured it out myself. Big thanks to benjessop whose suggestion didn't work, but did set me on the right train of thought.
validates :gender, numericality: { integer_only: true, greater_than_or_equal_to: 0, less_than: Enums::Gender::GENDERS.count }
I'll probably write a custom validation to implement that logic into several different value object enums. Thanks again for those that tried to help.
In your model file person.rb:
enum gender: Enums::Gender::GENDERS
But, In your model file gender.rb:
the constant is GENDER
Change the line in person.rb to:
enum gender: Enums::Gender::GENDER
instead of
enum gender: Enums::Gender::GENDERS

Uniqueness activerecord validation in a list for a user

I have a user model and a Request model,
class Request < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :status, inclusion: { in: %w(PENDING
RUNNING
COMPLETED
FAILED
ERROR) }
validates :status, on: :create,
uniqueness: { scope: :user_id,
message: "There is another request for this user",
conditions: -> { where(status: ['PENDING',
'RUNNING',
'ERROR']) }
}
end
The idea behind the uniqueness validation is that you cannot create a Request for that user if he has another PENDING, RUNNING or ERROR request.
it 'cannot a new pending request if user allredy has another RUNNING task' do
described_class.create!(user: user, status: 'RUNNING')
expect { described_class.create!(user: user) }.to raise_error(ActiveRecord::RecordInvalid,
/There is another request for this user/)
end
This test fails because It can create another Request even when another RUNNING request was created.
I guess that is because I doing something wrong on the condition.
This is the migration that creates Request model.
class CreateRequests < ActiveRecord::Migration
def up
create_table :requests do |t|
t.integer :reason
t.string :status, default: 'PENDING', :limit => 15
t.integer :user_id
t.timestamps null: false
end
end
def down
drop_table :requests
end
end
Your issue is that you're trying to do too much with the validates ... uniqueness: built-in method. The method assumes that the column value must be unique, not unique within a collection.
(Your attempt to define this within the conditions parameter seems reasonable, but this only actually limits the scope of records being compared against; it does not alter the definition of "unique column" to mean "unique within a list".)
I would suggest instead writing this validation as a custom method - for example, something like:
validate :unique_status_within_collection, on: :create
def unique_status_within_collection
unique_statuses = ['PENDING', 'RUNNING', 'ERROR']
if unique_statuses.include?(status) && Request.exists?(user: user, status: unique_statuses)
errors.add(:status, "There is another request for this user")
end
end
I would solve this by using an ActiveRecord::Enum instead. First make requests.status an integer column with an index instead of varchar.
class CreateRequests < ActiveRecord::Migration
def change
create_table :requests do |t|
t.integer :reason
t.integer :status, default: 0, index: true
t.belongs_to :user, foreign_key: true
t.timestamps null: false
end
add_index :requests, [:user_id, :status]
end
end
You should also use t.belongs_to :user, foreign_key: true to setup a foreign key for the user association. Specifying up and down blocks is not needed - just specify the change block and ActiveRecord is smart enough to know how to reverse it on its own.
class Request < ActiveRecord::Base
# this defaults to true in Rails 5.
belongs_to :user, required: true
enum status: [:pending, :running, :completed, :failed, :error]
validate :unique_status_within_collection, on: :create
def unique_status_within_collection
unique_statuses = %w[ pending running error]
if unique_statuses.includes?(self.status) && user.requests.exists?(status: unique_statuses)
errors.add(:base, "There is another request for this user")
end
end
end
One huge gotcha though is that the name request already is really significant in rails and if you use the instance variable #request in the controller you're masking the incoming request object.
I would rename it something like "UserRequest" or whatever to avoid this ambiguity.

How to make sure that relation is unique from both sides of many to many

I have a model that acts as a many to many relation. Class name is RelatedDocument which is self explanatory, I basically use it to related Document class instances with one to another.
I ran into issues with validations so for example I have this in RelatedDocument class:
validates :document, presence: true, uniqueness: { scope: :related_document }
validates :related_document, presence: true
This works I m unable to create duplicate document_id/related_document_id row. However if I wanted to make this unique from the other side of the relation and change the validation to this :
validates :document, presence: true, uniqueness: { scope: :related_document }
validates :related_document, presence: true, uniqueness: { scope: :document }
This does not work the same from the other side. I was writing rspec test when I noticed this. How can I write validation or a custom validation method that prevents saving of the same id combination, no matter from which side they are?
Update
Per first comment in the comment section saying that the first uniqueness validation will take care of the boths sides, I say it doesn't simply because my rspec test fails, here are they :
describe 'relation uniqueness' do
let!(:base_doc) { create(:document) }
let!(:another_doc) { create(:document) }
let!(:related_document) { described_class.create(document: another_doc, related_document: base_doc) }
it 'raises ActiveRecord::RecordInvalid, not allowing duplicate relation links' do
expect { described_class.create!(document: another_doc, related_document: base_doc) }
.to raise_error(ActiveRecord::RecordInvalid)
end
it 'raises ActiveRecord::RecordInvalid, not allowing duplicate relation links' do
expect { described_class.create!(document: base_doc, related_document: another_doc) }
.to raise_error(ActiveRecord::RecordInvalid)
end
end
Second test fails.
In case you'd like to consider an alternative to the custom validation that has already been provided, you could create reciprocal relationships each time a new record is added and use your existing validation.
For example, when I say that "Document A is related to Document B", I would then also insert a record stating that "Document B is related to Document A". You can keep your validations simple, and can more easily implement logic in the future for when you don't want a reciprocal relationship in some cases (perhaps the relationship is only reciprocated if the documents are from a different author).
Here is an untested example of the kind of model callbacks you would implement:
create_table :documents do |t|
t.string :title, null: false
end
create_table :related_documents do |t|
t.integer :source_document_id, null: false
t.integer :related_document_id, null: false
end
add_index :related_documents, [:source_document_id, :related_document_id], unique: true
add_foreign_key :related_documents, :documents, column: :source_document_id
add_foreign_key :related_documents, :documents, column: :related_document_id
class Document < ActiveRecord::Base
# We have many document relationships where this document is the source document
has_many :related_documents, foreign_key: :source_document_id
validates :title,
presence: true
end
class RelatedDocument < ActiveRecord::Base
after_create :add_reciprocal_relationship
after_destroy :remove_reciprocal_relationship
belongs_to :source_document, class_name: Document
belongs_to :related_document, class_name: Document
validates :source_document,
presence: true
validates :related_document,
presence: true,
uniqueness: {
scope: :source_document
}
private
# Creates a reciprocal relationship
def add_reciprocal_relationship
RelatedDocument.find_or_create_by(
related_document: self.source_document,
source_document: self.related_document
)
end
# Safely removes a reciprocal relationship if it exists
def remove_reciprocal_relationship
RelatedDocument.find_by(
related_document: self.source_document,
source_document: self.related_document
)&.destroy
end
end
A custom validator should work here
validate :unique_document_pair
def unique_document_pair
if RelatedDocument.exists?(:document => [self.document,self.related_document], :related_document => [self.document, self.related_document])
errors.add :base, "error"
end
end

Migration: How to make it an index while still allowing for nil values?

In my migration file I have a variable whose value should be unique or nil. How can I achieve such? My current setup generates all sorts of validation errors, I think because nil values are not unique and in the current set up it wants to see a unique value.
I currently have:
Migration file:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :my_var
...
end
end
end
class AddIndex < ActiveRecord::Migration
def change
add_index :users, :my_var, unique: true
end
end
Model file:
validates :my_var, uniqueness: true
Is there a way to allow it to be nil, to require a unique value if it has a value, and to make it an index?
As for your model validation, you can make it like this:
validates :my_var, uniqueness: { allow_nil: true }
OR, if you want to include empty strings (i.e. "")
validates :my_var, uniqueness: { allow_blank: true }
But, in any case, you'll have to drop your unique index
EDIT: The index part may not be necessary, as noted in the comments below.

Rails won't recognize setting a variable to 'false'?

I have the following (simplified) model and migration:
Model:
class User < ActiveRecord::Base
attr_readonly :contacted
validates :contacted, :inclusion => { :in => [true, false] }
def set_contacted
self.contacted = true
end
def unset_contacted
# self.contacted = false
self.contacted = "0"
end
end
Migration:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.boolean :contacted, :null => false, :default => false
t.timestamps
end
end
end
As you can kind of see in the comment in my model, setting the variable contact to false results in an error - I can only set it to "0". Why? I don't see how "false" would violate the null constraint, right?
Edit:
For clarification, I am using PostgreSQL and ActiveRecord. The error that I'm getting is this:
C:/Ruby193/lib/ruby/gems/activerecord-3.2.8/lib/active_record/validations.rb:56:in 'save!' Validation failed: ActiveRecord::RecordInvalid)
I get that error even if I remove the "validates" statement from my model, and even if I remove the NULL constraint from the migration. It's something to do with setting the value of the attribute to be false. Is there some odd constraint on ActiveRecord booleans?
It's a bit difficult answering your question without having the specific error information.
First I'd change attr_readonly to attr_accessible - So the field will be updatable.
Secondly, I'd re-write your method:
def unset_contacted
self.contacted = false
self.save! # Saving your methods (the ! is for throwing an exception if it fails).
end
No one seems able to solve this, but it' no longer an issue for me. My model is better served by using a state_machine gem, so I removed this field altogether.

Resources