Efficient way to report record validation warnings as well as errors?
Asked Answered
P

3

20

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...

Palate answered 8/7, 2014 at 9:44 Comment(5)
Have you had a look at the accepted answer of this question? #3342949 It seem that you can create a separate ActiveModel::Errors and use it as storage for your warningsEuonymus
That's definitely interesting, Benjamin, thank you. That approach does mean that we would have to write separate matchers for warnings as opposed to those that produce errors, even if their internals are identical. It's possible I could live with that, although the potential for code duplication exists. A means by which to repurpose, say, Rails' built-in Length validation matcher to be able to output warnings instead of errors, based on calling context... But I'm prepared to be pragmatic if it gets me where I need to be!Palate
I haven't been deep into it, but yes it looks like your will have to duplicate all of those validations classes. Quick glance at rails source confirm it: record.errors.add(attribute, ...) where you will need record.warnings.add(attribute, ...)Euonymus
Also, it may be 3 years old now, but perhaps you can find some inspiration in this gem github.com/stevehodgkiss/validation-scopesEuonymus
Yeah, you may be right about having to rewrite all matchers. I'd been toying around with the possibility of wrapping my base class in a decorator, with its own errors object to capture some validations and then expose them as a separate warnings object – but that just seems to shovel up a huge amount of pain as a means of avoiding a little bit of potential duplication…Palate
A
18

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).

Andrea answered 14/8, 2014 at 3:47 Comment(1)
Thanks! Exactly what I was looking for! Works great with Rails 4 too. I have used it together with Rails error validations.Slab
D
7

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

Disquisition answered 17/9, 2019 at 14:22 Comment(0)
G
2

Another options is to have a seperate object for warnings as such:

class MyModelWarnings < SimpleDelegator
  include ActiveModel::Validations

  validates :name, presence: true

  def initialize(model)
    super
    validate
  end  

  def warnings; errors; end
end

class MyModel < ActiveRecord::Base
  def warnings
    @warnings ||= MyModelWarnings.new(self).warnings
  end
end
Goldia answered 6/12, 2019 at 11:12 Comment(1)
Anyone coming to this page for the solution? Look at this one before trying others. This is a much friendlier code and works, too. If I could, I would vote for this as the answer.Psychophysiology

© 2022 - 2024 — McMap. All rights reserved.