From b5a4609b567c874dabfb60a6e358c0e6e2638586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Wed, 23 Mar 2022 16:03:36 +0100 Subject: [PATCH] Make it easier to customize validations There are CONSUL installations where the validations CONSUL offers by default don't make sense because they're using a different business logic. Removing these validations in a custom model was hard, and that's why in many cases modifying the original CONSUL models was an easier solution. Since modifying the original CONSUL models makes the code harder to maintain, we're now providing a way to easily skip validations in a custom model. For example, in order to skip the price presence validation in the Budget::Heading model, we could write a model in `app/models/custom/budget/heading.rb`: ``` require_dependency Rails.root.join("app", "models", "budget", "heading").to_s class Budget::Heading skip_validation :price, :presence end ``` In order to skip validation on translatable attributes (defined with `validates_translation`), we have to use the `skip_translation_validation` method; for example, to skip the proposal title presence validation: ``` require_dependency Rails.root.join("app", "models", "proposal").to_s class Proposal skip_translation_validation :title, :presence end ``` Co-Authored-By: taitus --- app/models/application_record.rb | 1 + app/models/concerns/skip_validation.rb | 26 ++++++++ config/initializers/globalize.rb | 4 ++ spec/models/skip_validation_spec.rb | 83 ++++++++++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 app/models/concerns/skip_validation.rb create mode 100644 spec/models/skip_validation_spec.rb diff --git a/app/models/application_record.rb b/app/models/application_record.rb index ebc177b0d..d145cec8a 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,5 +1,6 @@ class ApplicationRecord < ActiveRecord::Base include HumanName + include SkipValidation self.abstract_class = true def self.sample(count = 1) diff --git a/app/models/concerns/skip_validation.rb b/app/models/concerns/skip_validation.rb new file mode 100644 index 000000000..2a3dbcdc1 --- /dev/null +++ b/app/models/concerns/skip_validation.rb @@ -0,0 +1,26 @@ +module SkipValidation + extend ActiveSupport::Concern + + module ClassMethods + def skip_validation(field, validator) + validator_class = if validator.is_a?(Class) + validator + else + "ActiveModel::Validations::#{validator.to_s.camelize}Validator".constantize + end + + _validators[field].reject! { |existing_validator| existing_validator.is_a?(validator_class) } + + _validate_callbacks.each do |callback| + if callback.raw_filter.is_a?(validator_class) + callback.raw_filter.instance_variable_set("@attributes", callback.raw_filter.attributes - [field]) + end + end + end + + def skip_translation_validation(field, validator) + skip_validation(field, validator) + translation_class.skip_validation(field, validator) + end + end +end diff --git a/config/initializers/globalize.rb b/config/initializers/globalize.rb index d5834f217..7d93e9fda 100644 --- a/config/initializers/globalize.rb +++ b/config/initializers/globalize.rb @@ -9,6 +9,10 @@ module Globalize end end end + + class Translation + include SkipValidation + end end end diff --git a/spec/models/skip_validation_spec.rb b/spec/models/skip_validation_spec.rb new file mode 100644 index 000000000..553432757 --- /dev/null +++ b/spec/models/skip_validation_spec.rb @@ -0,0 +1,83 @@ +require "rails_helper" + +describe SkipValidation do + describe ".skip_validation" do + before do + dummy_model = Class.new do + include ActiveModel::Model + include SkipValidation + attr_accessor :title, :description + + validates :title, presence: true, length: { in: 10..60, allow_nil: true } + validates :description, presence: true + end + + stub_const("DummyModel", dummy_model) + end + + it "accepts validator classes as parameters" do + DummyModel.skip_validation :title, ActiveModel::Validations::PresenceValidator + + expect(DummyModel.new(title: nil, description: "Something")).to be_valid + end + + it "accepts symbols as parameters" do + DummyModel.skip_validation :title, :presence + + expect(DummyModel.new(title: nil, description: "Something")).to be_valid + end + + it "does not affect other attributes" do + DummyModel.skip_validation :title, :presence + + expect(DummyModel.new(title: nil, description: nil)).not_to be_valid + end + + it "does not affect other validations" do + DummyModel.skip_validation :title, :presence + + expect(DummyModel.new(title: "Short", description: "Something")).not_to be_valid + end + + it "works with validators other than presence" do + DummyModel.skip_validation :title, :length + + expect(DummyModel.new(title: "Short", description: "Something")).to be_valid + expect(DummyModel.new(title: nil, description: "Something")).not_to be_valid + end + end + + describe ".skip_translation_validation" do + before do + dummy_banner = Class.new(Banner) do + def self.translation_class + @translation_class ||= Class.new(Banner::Translation) { clear_validators! } + end + reflect_on_association(:translations).options[:class_name] = "DummyBanner::Translation" + + clear_validators! + validates_translation :title, presence: true + validates_translation :description, presence: true + end + + stub_const("DummyBanner", dummy_banner) + stub_const("DummyBanner::Translation", dummy_banner.translation_class) + end + + it "removes the validation from the translatable attribute" do + DummyBanner.skip_translation_validation :title, :presence + + custom_banner = DummyBanner.new(build(:banner).attributes.merge(title: nil)) + + expect { custom_banner.save! }.not_to raise_exception + end + + it "does not affect other validations" do + DummyBanner.skip_translation_validation :title, :presence + + custom_banner = DummyBanner.new(build(:banner).attributes.merge(description: nil)) + + expect { custom_banner.save! }.to raise_exception(ActiveRecord::RecordInvalid) + end + end +end