Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficient way to report record validation warnings as well as errors?

I've got a Rails project where, as in most apps, we have a number of hard-and-fast validation rules to which all objects must conform before being persisted. Naturally, ActiveModel's Validations are perfect for that – we're using a combination of Rails defaults and our own hand-rolled validations.

More and more, though, we're coming up against use cases where we would like to alert the user to cases where, while their data is not invalid in the strictest sense, there are elements which they should review, but which shouldn't in themselves prevent record persistence from occurring. A couple of examples, off the top of my head:

  • A post title has been submitted in ALL CAPS, which may be valid but probably isn't
  • A pice of body text is more than x number of words less or more than a suggested word count

The validations module is such a good metaphor for how we treat validation errors – and has so many matchers already available – that ideally I'd like to be able to reuse that basic code, but to generate a collection of warnings items alongside errors. This would allow us to highlight those cases differently to our users, rather than implying that possible violations of house style are equivalent to more egregious, strictly enforced rules.

I've looked at gems such as activemodel-warnings, but they work by altering which matchers are checked when the record is validated, expanding or shrinking the errors collection accordingly. Similarly, I looked at the built-in :on parameter for validations to see if I could hand-roll something, but again all violations would end up in an errors collection rather than separated out.

Has anybody tried anything similar? I can't imagine I'm the only one who'd like to achieve this goal, but am drawing a blank right now...

like image 472
Scott Matthewman Avatar asked Jul 08 '14 09:07

Scott Matthewman


2 Answers

Here is some code I wrote for a Rails 3 project that does exactly what you're talking about here.

# Define a "warnings" validation bucket on ActiveRecord objects.
#
# @example
#
#   class MyObject < ActiveRecord::Base
#     warning do |vehicle_asset|
#       unless vehicle_asset.description == 'bob'
#         vehicle_asset.warnings.add(:description, "should be 'bob'")
#       end
#     end
#   end
#
# THEN:
#
#   my_object = MyObject.new
#   my_object.description = 'Fred'
#   my_object.sensible? # => false
#   my_object.warnings.full_messages # => ["Description should be 'bob'"]
module Warnings
  module Validations
    extend ActiveSupport::Concern
    include ActiveSupport::Callbacks

    included do
      define_callbacks :warning
    end

    module ClassMethods
      def warning(*args, &block)
        options = args.extract_options!
        if options.key?(:on)
          options = options.dup
          options[:if] = Array.wrap(options[:if])
          options[:if] << "validation_context == :#{options[:on]}"
        end
        args << options
        set_callback(:warning, *args, &block)
      end
    end

    # Similar to ActiveModel::Validations#valid? but for warnings
    def sensible?
      warnings.clear
      run_callbacks :warning
      warnings.empty?
    end

    # Similar to ActiveModel::Validations#errors but returns a warnings collection
    def warnings
      @warnings ||= ActiveModel::Errors.new(self)
    end

  end
end

ActiveRecord::Base.send(:include, Warnings::Validations)

The comments at the top show how to use it. You can put this code into an initializer and then warnings should be available to all of your ActiveRecord objects. And then basically just add a warnings do block to the top of each model that can have warnings and just manually add as many warnings as you want. This block won't be executed until you call .sensible? on the model.

Also, note that since warnings are not validation errors, a model will still be technically valid even if it isn't "sensible" (as I called it).

like image 175
pdobb Avatar answered Nov 11 '22 00:11

pdobb


Years later, but in newer Rails versions there's a bit easier way:

  attr_accessor :save_despite_warnings

  def warnings
    @warnings ||= ActiveModel::Errors.new(self)
  end

  before_save :check_for_warnings
  def check_for_warnings
    warnings.add(:notes, :too_long, count: 120) if notes.to_s.length > 120

    !!save_despite_warnings
  end

Then you can do: record.warnings.full_messages

like image 5
Marcin Raczkowski Avatar answered Nov 11 '22 02:11

Marcin Raczkowski