I am currently working on a Rails 3 application that has a form that takes one parent object with a couple of pre-defined attributes selected from a fairly large array. Each parent object has several child objects that depending on which pre-defined attributes you have selected in the parent, can have WILDLY different min-max values, each with their own separate often unique limitations.
I am quickly realizing that validating these objects is going to result in an extremely large model file. I was wondering if there was a proper way to remove this type of large scale validation from the model(or at least have the model point to somewhere else).
Also a more higher level question, is it normal to have a say... 1000 lines of code to validate the integrity of the data of an object?
1) You can create a custom validator class, which involves inheirting from ActiveModel::Validator and implementing a validate method that takes the records to validate:
class Report > ActiveRecord::Base
validates with MyValidator
end
class MyValidator < ActiveModel::Validator
def validate(record)
record.errors[:base] = << "Error" unless is_valid(record)
end
end
2) In rails 3, there are validation macros as wel, which means extending ActiveModel::EachValidator:
class Report < ActiveRecord::Base
validates :name :report_like => true
end
class ReportLikeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value["Report"]
record.errors.add attribute, "Does not appear to be a ..."
end
end
Related
I have a Rails 5 application where users can enter currency values in different fields and different models.
Since I only serve one locale, I want users to be able to enter decimals using both . and , decimal separators and ignore any thousands separator.
For example, my users might enter: 1023.45 or 1023,45 but never 1.023,45.
Simple solution that fits my use case: On specific decimal fields representing currency, replace , with . using gsub(',', '.').
What is the best place to put this code? There are multiple models with differently named fields that need to use this code.
Preferably, I would use something like a custom validator that I create once and simply reference with 1 line from all models with the appropriate field. Very much like this example of a custom validator that checks whether an entered value is positive:
class PositiveValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value and BigDecimal.new(value).negative?
record.errors[attribute] << 'is negative'
end
end
end
And then in the model reference it with:
validates :total, positive: true, allow_blank: true
However, this is ofcourse for validations, which shouldn't modify data. I tried to do this with concerns, but from the examples I have seen I should already know which fields are being transformed.
Do I really need to write in each model something like this:
before_validation lambda {self.total.gsub!(',', '.') if self.total}
or is there a more elegant Rails-like solution?
edit: again, note that field name 'total' might be different in each model
You could use custom type-casting starting from Rails 4.2
class LocalFloat < ActiveRecord::Type::Float
def cast_value(value)
value = value.gsub(',', '.') if value.is_a? String
super(value)
end
end
class Order < ApplicationRecord
attribute :total, LocalFloat.new
end
o = Order.new total: '5,5'
o.total # => 5.5
What's an elegant way to handle passing around model errors through chains of classes?
Let's say I have a complex form with inputs foo and bar for which I want to display validation errors on the form. However, foo and bar themselves are not model inputs, they are just data passed to service classes:
class ComplexForm
attr_accessor :foo, :bar
def save
foo_processor.process(foo)
bar_processor.process(bar)
end
end
The processors then validate the inputs, validate the resulting model, and either save or return errors. The processors use ActiveModel::Model
class FooProcessor
include ActiveModel::Model
validate :foo_is_valid
def process(foo)
model_instance = MyModel.new(parse(foo))
if valid? && model_instance.valid?
model_instance.save
else
???
end
end
end
How do I pass this potential chain of errors up to ComplexForm and attribute them to the right input?
I have this model which has a mandatory field which needs to be automatically set just before save. I'm struggling with the correct way to implement this:
build the logic in the controller before the save (and have validates rule in model)
build the logic in a before_save callback and have validates rule in model, but this seems to late in the flow? I do get validation errors this way.
build the logic in a before_save callback and don't define validation for this particular field
do it any of the ways above and don't assign a validates rule for the particular field
I was working on 2 since this seems like the correct way to implement this. Was considering the usage of before_validation, but I don't know what would happen when my other fields don't get validated... this could cause double assignment of the same value..
code for 2 which gives a basic idea of what I'm trying to achieve:
#category.rb
class Category < ActiveRecord::Base
before_create :set_position_number
def set_position_number
highest = Category.maximum(:position)
self.position = highest.to_i + 1
end
end
I'm struggling with the correct way to implement this
The most efficient way will be to use an ActiveRecord callback hook, such as you've posted:
#app/models/category.rb
class Category < ActiveRecord::Base
before_create :your_action
private
def your_action
#fires before create
end
end
but this seems to late in the flow
As mentioned in the comments, you can see the order of the callbacks (and thus their order in the flow):
Thus, if you want to populate some data before you validate, and then validate that data, you'll be best using the before_validation callback:
#app/models/category.rb
class Category < ActiveRecord::Base
before_validation :set_position_number, on: :create
validates :position, ______________
private
def set_position_number
highest = Category.maximum(:position)
self.position = highest.to_i + 1
end
end
Remember, a Rails model just populates certain attributes which are then to be either saved to the db, or validated. Rails does not care where those attributes come from; populating them before_validation is a good a source as the controller.
If you are setting a value automatically and don't take user input, you don't need validation. Write a unit test.
If the field is something like a position value, then you should indeed set it in a before_create callback.
Where do the basic validators lie when dealing with Form objects and regular Rails models?
Following the concept of decoupling forms from the persistence layer in Rails. I've setup a Form Object Cage that creates two objects together... say Animal and Plant.
Following Form Object examples from http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ or https://github.com/solnic/virtus or https://github.com/makandra/active_type , each of these show the Form object itself has validations... no problem... part of the benefits include being able to validate objects in a more contextually aware way.
The issue:
class Animal < ActiveRecord::Base
validates :color, presence: true
validate :only_one_brown
private
def only_one_brown
if some_complex_thing
errors.add(:color, 'can not have more than one brown animal.')
end
end
end
class Plant < ActiveRecord::Base
validates :color, presence: true
end
class Cage
include Virtus.model # or ActiveType or whatever
include ActiveModel::Validations
attribute :bird_color, String
attribute :plant_color, String
validates :bird_color, presence: true
validates :plant_color, presence: true
def save
if valid?
animal.save!
plant.save!
true
else
false
end
end
def animal
#animal ||= Animal.new(color: bird_color)
end
def plant
#plant ||= Plant.new(color: plant_color)
end
end
How do I validate animal's "only one brown" rule without:
Too much duplication.
A lot of code to make Cage still act like an AR model
If we don't duplicate the validation code, when "only one brown" is false, Cage doesn't have an error for it... we'll raise, which requires the controller to catch and handle, which is bad.
If we do duplicate the code, and if there are several custom validations, we're duplicating a lot of code and each other form object that deals with Animal needs the duplicated validations now.
If we move the validation code out of Animal into Cage entirely, similar issue: all objects that interact with Animal need to know about the "only one brown" rule, which is just duplicating validators and opening up an easy way to forget to enforce it somewhere.
If we move Animal's error array up to Cage's, Animal's error is on :color, which is ambiguous to Cage, and shows an error on an attribute name the client never sent in. If you want to map Animal's error keys to Cage's, now you need to keep an map for each Form Object, feels stinky.
Are there any good patterns or ways to deal with this situation? I feel like it is very common when you start using Form Objects but all examples are quite trivial.
Thanks in advance!
At the end of point 3 on http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ the author says: "As a bonus, since validation logic is often contextual, it can be defined in the place exactly where it matters instead of needing to guard validations in the ActiveRecord itself." I'm agree with Bryan Helmkamp, puts the validation where it matters, you don't need to duplicate it.
edited:
If I were you, I'll put the validation only on the ActiveRecord model. And I'll update the Cage class:
def save
if valid?
ActiveRecord::Base.transaction do
animal.save!
plant.save!
end
true
else
false
end
rescue Exception => exception
raise if valid?
false
end
And I'll add an errors method that returns the errors of Cage, Plant and Animal instances.
edited:
I think you can redefine the valid? method, and then errors works fine:
class Cage
include ActiveModel::Model
def valid_with_mymodels?
valid_without_mymodels? && animal.valid? && plant.valid?
animal.errors.each do |attribute, error|
self.errors.add :"bird_#{attribute.to_s}", error
end
plant.errors.each do |attribute, error|
self.errors.add :"plant_#{attribute.to_s}", error
end
errors.empty?
end
alias_method_chain :valid?, :mymodels
...
end
Just, be careful with the name of your attrs.
I'm not sure how works Virtus, with Rails 4 you can use ActiveModel::Model, if using rails 3 I need research.
edited:
If you are using Rails 3.2, you can't use ActiveModel::Model, but you get the same with this:
class Cage
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
...
end
I'm working with a nested form that encompasses a total of 7 models, each with different validations. When simply editing the form, the validations run and display fine, and data is saved properly. However, I need to have different validations run depending on who is submitting the form (ie, admins can skip some otherwise required fields).
I thought I could get certain validations to be skipped by using attr_accessible :editing_user in my models, then set this within the controller.
class ModelExample < ActiveRecord::Base
attr_accessible :editing_user
validates_presence_of :email, :unless => "editing_user == 'admin'"
end
class ModelExamplesController < ActionController::Base
def create
#model_example = ModelExample.new(params[:model_example])
#model_example.editing_user = 'admin'
#model_example.save
end
end
I used this basic structure within the nested models, checking to see if I could save properly. This is where the weird behavior starts. For some reason, it looks like ActiveRecord is trying to save nested models multiple times, running validations each time. What makes this odd is I call #model_example.save, which should just return false if it fails. But, the first validation goes through (since editing_user is set), but later validations fail and raise exceptions, so the normal .save methods ends up raising an exception instead of returning.
Does anyone know how to either avoid having ActiveRecord do all extra validations and saves, or how to persist editing_user across those duplicate actions?
ha! just did this yeasterday, well, almost same use case anyway (persist the user). Here is how i solved it (with all credit to my buddy Jason Dew that I copied from):
class User < ActiveRecord::Base
module ClassMethods
attr_accessor :current
end
extend ClassMethods
end
This code block adds a singleton accessor :current to the User class methods, and can be called as User.current. nicer looking that a method called self.currrent
then in the app controller
before_filter :require_user #=> which in this case goes off and sets the current_user var
before_filter {|c| User.current = current_user}
which passes the app controller to the block and sets the User.current var.
Then in any other model
class MyClass < ActiveRecord::Base
def log
"This was done by #{User.current}"
end
end