Skip validation from include element - ruby-on-rails

I am having an issue, I have the next class
class Question < ApplicationRecord
include Mappable
...
end
so my problem is at the time to create a Question, I need to keep the Question validations but skip the ones that are incoming from Mappable
because by now I am using question.save(validate: false) and I have to update that to something like question.save(mappable_validate: false), just to skip the validations from Mappable
EDIT
Mappable:
module Mappable
extend ActiveSupport::Concern
included do
attr_accessor :skip_map
has_one :map_location, dependent: :destroy
accepts_nested_attributes_for :map_location, allow_destroy: true, reject_if: :all_blank
validate :map_must_be_valid, on: :create, if: :feature_maps?
def map_must_be_valid
return true if skip_map?
unless map_location.try(:available?)
skip_map_error = "Map error"
errors.add(:skip_map, skip_map_error)
end
end
def feature_maps?
Setting["feature.map"].present?
end
def skip_map?
skip_map == "1"
end
end
end

There are quite a few ways to solve this. But no reliable ones that don't involve modifying the module.
One would be simply to use composition and move the validations to its own module:
module Mappable
module Validations
extend ActiveSupport::Concern
included do
validate :map_must_be_valid, on: :create, if: :feature_maps?
end
end
end
class Question < ApplicationRecord
include Mappable
end
class Foo < ApplicationRecord
include Mappable
include Mappable::Validations
end
Another very common way to make the behavior provided by a module customizeable is to not just cram all your code into the Module#included hook which doesn't let you pass options.
Instead create a class method:
module Mappable
extend ActiveSupport::Concern
def map_must_be_valid
return true if skip_map?
unless map_location.try(:available?)
skip_map_error = "Map error"
errors.add(:skip_map, skip_map_error)
end
end
def feature_maps?
Setting["feature.map"].present?
end
def skip_map?
skip_map == "1"
end
module ClassMethods
def make_mappable(validate: true)
attr_accessor :skip_map
has_one :map_location, dependent: :destroy
accepts_nested_attributes_for :map_location,
allow_destroy: true, reject_if: :all_blank
if validate
validate :map_must_be_valid, on: :create, if: :feature_maps?
end
end
end
end
And then just call the class method in the class you want to modify.
class Question < ApplicationRecord
include Mappable
make_mappable(validate: false)
end
This pattern can be found everywhere in Rails and Ruby in general and lets you make the functionality you're providing much flexible.
I understand that this might not seem to be immediately helpful as the code is coming from a gem. But it can help you understand what to do to fix the gem or evaluate if its actually worthwhile/needed.

Related

Rails model common validations inherited from abstract base class but unique field validations occur on subclass

I'm trying to understand if it's possible, given two models that share some methods and fields, to put the validations that are common between the two of them in an abstract base class. Below code represents a simplified version of my situation.
There are two classes of invoice line items: sales and collections. These line items share a common field invoice_amount I want to validate the presence of the invoice_amount from an abstract base class but fields that are not common to both models get validated by the subclass.
class Collection < InvoiceLineItem
belongs_to :invoice
validates :c_number, :invoice_number, :invoice_date, presence: true
.
.
.
end
class Sale < InvoiceLineItem
belongs_to :invoice
.
.
.
end
class InvoiceLineItem < ApplicationRecord
self.abstract_class = true
def self.inherited(base)
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
def invoice_amount
self[:invoice_amount] || '0.00'
end
def export_date
invoice_date
end
end
I've tried several things to get this to work with no success. Some of my attempts included adding the following code to my InvoiceLineItem base class
def self.inherited(base)
base.class_eval do
validates :invoice_amount, presence: true
end
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
and
def self.inherited(base)
base.class_eval do
base.send(:validates, :invoice_amount, presence: true)
end
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
and this as described here (https://medium.com/#jeremy_96642/deep-rails-how-to-use-abstract-classes-6aee9b686e75) which seemed promising because it described exactly what I want to do however it does not work for me.
with_options presence:true do
validates :invoice_amount
end
In all these cases the code executes without error however if I write a test like below it fails because validation succeeds!
RSpec.describe Collection, type: :model do
it "Requires an invoice amount" do
result = Collection.create(invoice_amount: nil, c_number: 'CUST012', invoice_number: 'INV001', invoice_date: Date.new(1999, 1,1))
expect(result.valid?).to be false
expect(result.errors[:invoice_amount]).to include("can't be blank")
end
end
I'm not really interested in hearing answers about how it should be done using composition instead of inheritance I won't go into the details but just assume that it has to be done using inheritance. It seems like it should be possible but I'm not sure and I can't find any source on the internet that has a solution that actually works.
Any help would be very much appreciated!
I figured out my issue, it turns out this code was working:
class InvoiceLineItem < ApplicationRecord
self.abstract_class = true
def self.inherited(base)
super
base.send(:extend, NumberFormatter)
base.send(:commafy, :invoice_amount)
end
with_options presence: true do
validates :invoice_amount
end
def invoice_amount
self[:invoice_amount] || 0.00
end
def export_date
invoice_date
end
def debtor_number_up_to_space
debtor_number.split[0]
end
end
the validator was using the method invoice_amount which was shadowing the field on the model so invoice_amount wasn't being seen as nil but as 0.00 thus my test validation was passing correctly.
Once I removed the || 0.00 I could get the test to fail.

Use of concern in rails 4

I tried to to use concerns in my project. I would like to build a method to get the date in french for every model I have. Here is my code. Currently, I get the error : wrong argument type Class (expected Module) at the line include DateTime in the model.
Here is my file models/concerns/date_time.rb
module DateTime
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
# methods defined here are going to extend the class, not the instance of it
module ClassMethods
def date_string
h = {1=>'Janvier',2=>'Février',3=>'Mars',4=>'Avril',5=>'Mai',6=>'Juin',7=>'Juillet',8=>'Août',9=>'Septembre',10=>'Octobre',11=>'Novembre',12=>'Décembre'}
"#{self.created_at.day}-#{h[self.created_at.month]}-#{self.created_at.year}"
end
end
end
Here is my file models/demands.rb
class Demand < ActiveRecord::Base
include DateTime
belongs_to :skill
belongs_to :project
belongs_to :user
has_many :transactions
validates :project, presence: true
validates :skill, presence: true
validates :title, presence: true
end
Thanks in advance for your help !
Use the Rails built in I18n functionality instead. Doing localization in the model layer is just wrong. Models should only be concerned with data and business logic - not how data (like dates) are presented.
Your immediate issue here is that, because DateTime is a class in the Ruby standard library, Ruby is trying to include that class, not your module. If you rename the module to something unique, say, UsesDateTime, your error should go away.
That said, for this particular method, I agree with max.

How do a custom method applicable to various models

I have the following method called capitalizeEachWord. Inside this method there is an attribute called company
class BusCompany < ActiveRecord::Base
attr_accessible :company
before_save :capitalizeEachWord
validates :company,presence: true,
uniqueness: { case_sensitive: false },
format: /^([a-zA-z0-9]+\s?){1,}$/
def capitalizeEachWord
self.company=self.company.downcase.split.map(&:capitalize).join(' ')
end
end
I would like that this method not use the attribute company directly, but receives this attribute as a parameter for doesn't do it dependent of the model BusCompany. Something as the following. The problem is that this method I going to use in various models and don't want to write it in each model but use the inheritance
class BusCompany < ActiveRecord::Base
attr_accessible :company
before_save :capitalizeEachWord(self.company)
validates :company,presence: true,
uniqueness: { case_sensitive: false },
format: /^([a-zA-z0-9]+\s?){1,}$/
def capitalizeEachWord(attribute)
self.attribute=self.attribute.downcase.split.map(&:capitalize).join(' ')
end
end
Add the following code into config/initializers/capitalizer.rb
module Capitalizer
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def capitalize(*attributes)
#attributes_to_capitalize = attributes
before_save :capitalize_each_word
end
def attributes_to_capitalize
Array.new(#attributes_to_capitalize)
end
end
def capitalize_each_word
self.class.attributes_to_capitalize.each do |attr|
if value = send(attr)
self.send("#{attr}=", value.strip.titleize)
end
end
end
end
And then in your class:
class BusCompany < ActiveRecord::Base
include Capitalizer
capitalize :company
...
end
First, I'd recommend you override the setter for company instead of using error prone callbacks, like this:
class BusCompany < ActiveRecord::Base
# you can also use #titleize instead of capitalize each word
# also use try, in case `arg` is nil
def company=(arg)
super arg.try(:titleize)
end
end
Then you can use modules to wrap this functionality into a reusable unit. Throw this in a file in your concerns folder, or just in to the models folder:
module CapitalizedSetter
def capitalize_setter(*attr_names)
# for each attr name, redifine the setter so it supers the titleized argument instead
attr_names.each do |attr|
define_method(:"#{attr}=") { |arg| super arg.try(:titleize) }
end
end
end
Finally extend it into the desired models:
class BusCompany
extend CapitalizedSetter
capitalized_setter :company
end

Accepts Nested Attributes & Filters

Let's say I have two models; Post & Comment
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
before_save :do_something
def do_something
# Please, let me do something!
end
end
I have a form for Post, with fields for comments. Everything works as expected, except for the filter. With the above configuration, before_save filter on Comment isn't triggered.
Could you explain why, and how I can fix this?
Rails doesn't instantiate and save the comments individually in this case. You would be better off adding a callback in your Post model to handle this for nested comments:
class Post < AR::Base
before_save :do_something_on_comments
def do_something_on_comments
comments.map &:do_something
end
end
According to Bryan Helmkamp, it's better to use the form object pattern than it is to use accepts_nested_attributes_for. Take a look at 7 Patterns to Refactor Fat ActiveRecord Models
Maybe you could do something like this?
class NewPost
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :post
attr_reader :comment
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#post = Post.create!
#comment = #post.comment.create!
end
end
do_something would get called when you create the comment.

ActiveRecord Problems using callbacks and STI

Hey folks, following problem with Rails and STI:
I have following classes:
class Account < AC::Base
has_many :users
end
class User < AC::Base
extend STI
belongs_to :account
class Standard < User
before_save :some_callback
end
class Other < User
end
end
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
type.new(*args, &block)
end
end
end
And now the problem:
Without rewriting User.new (in module STI), the callback inside User::Standard gets never called, otherwise the account_id is always nil if I create users this way:
account.users.create([{ :type => 'User::Standard', :firstname => ... }, { :type => 'User::Other', :firstname => ... }])
If I'm using a different approach for the module like:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
Then instance variables are not shared, because it's creating a new object.
Is there any solution for this problem without moving the callbacks to the parent class and checking the type of class?
Greetz
Mario
Maybe there's something I don't know, but I've never seen Rails STI classes defined in that manner. Normally it looks like...
app/models/user.rb:
class User < AC::Base
belongs_to :account
end
app/models/users/standard.rb:
module Users
class Standard < User
before_save :some_callback
end
end
app/models/users/other.rb:
module Users
class Other < User
end
end
It looks as though you are conflating class scope (where a class "lives" in relation to other classes, modules, methods, etc.) with class inheritance (denoted by "class Standard < User"). Rails STI relationships involve inheritance but do not care about scope. Perhaps you are trying to accomplish something very specific by nesting inherited classes and I am just missing it. But if not, it's possible it's causing some of your issues.
Now moving on to the callbacks specifically. The callback in Standard isn't getting called because the "account.users" relationship is using the User class, not the Standard class (but I think you already know that). There are several ways to deal with this (I will be using my class structure in the examples):
One:
class Account
has_many :users, :class_name => Users::Standard.name
end
This will force all account.users to use the Standard class. If you need the possibility of Other users, then...
Two:
class Account
has_many :users # Use this to look up any user
has_many :standard_users, :class_name => Users::Standard.name # Use this to look up/create only Standards
has_many :other_users, :class_name => Users::Other.name # Use this to look up/create only Others
end
Three:
Just call Users::Standard.create() and Users::Other.create() manually in your code.
I'm sure there are lots of other ways to accomplish this, but there are probably the simplest.
So I solved my problems after moving my instance variables to #attributes and using my second approach for the module STI:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
class User < AR:Base
extend STI
belongs_to :account
validates :password, :presence => true, :length => 8..40
validates :password_digest, :presence => true
def password=(password)
#attributes['password'] = password
self.password_digest = BCrypt::Password.create(password)
end
def password
#attributes['password']
end
class Standard < User
after_save :some_callback
end
end
Now my instance variable (the password) is copied to the new User::Standard object and callbacks and validations are working. Nice! But it's a workaround, not really a fix. ;)

Resources