web-dev-qa-db-fra.com

Messages d'erreur de validation Rails: affichage d'un seul message d'erreur par champ

Rails affiche tous les messages d'erreur de validation associés à un champ donné. Si j'ai trois validates_XXXXX_of :email et que je laisse le champ vide, trois messages apparaissent dans la liste des erreurs.

Exemple:

validates_presence_of :name
validates_presence_of :email
validates_presence_of :text

validates_length_of :name, :in => 6..30
validates_length_of :email, :in => 4..40
validates_length_of :text, :in => 4..200

validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i<br/>

<%= error_messages_for :comment %> me donne:

7 errors prohibited this comment from being saved

There were problems with the following fields:

Name can't be blank
Name is too short (minimum is 6 characters)
Email can't be blank
Email is too short (minimum is 4 characters)
Email is invalid
Text can't be blank
Text is too short (minimum is 4 characters)

Il est préférable d’afficher un message à la fois. Existe-t-il un moyen facile de résoudre ce problème? Il semble facile d’avoir une condition du type: Si vous rencontrez une erreur pour :email, arrêtez de valider :email et passez à l’autre champ.

31
TraderJoeChicago

Bert sur RailsForum a écrit à ce sujet il y a quelque temps. Il a écrit le code ci-dessous et j'ai ajouté quelques modifications mineures pour qu'il fonctionne sous Rails-3.0.0-beta2.

Ajoutez ceci à un fichier appelé app/helpers/errors_helper.rb et ajoutez simplement helper "errors" à votre contrôleur.

module ErrorsHelper

  # see: lib/action_view/helpers/active_model_helper.rb
  def error_messages_for(*params)
        options = params.extract_options!.symbolize_keys

        objects = Array.wrap(options.delete(:object) || params).map do |object|
          object = instance_variable_get("@#{object}") unless object.respond_to?(:to_model)
          object = convert_to_model(object)

          if object.class.respond_to?(:model_name)
            options[:object_name] ||= object.class.model_name.human.downcase
          end

          object
        end

        objects.compact!
        count = objects.inject(0) {|sum, object| sum + object.errors.count }

        unless count.zero?
          html = {}
          [:id, :class].each do |key|
            if options.include?(key)
              value = options[key]
              html[key] = value unless value.blank?
            else
              html[key] = 'errorExplanation'
            end
          end
          options[:object_name] ||= params.first

          I18n.with_options :locale => options[:locale], :scope => [:errors, :template] do |locale|
            header_message = if options.include?(:header_message)
              options[:header_message]
            else
              locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ')
            end

            message = options.include?(:message) ? options[:message] : locale.t(:body)

            error_messages = objects.sum do |object|
              object.errors.on(:name)
              full_flat_messages(object).map do |msg|
                content_tag(:li, ERB::Util.html_escape(msg))
              end
            end.join.html_safe

            contents = ''
            contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
            contents << content_tag(:p, message) unless message.blank?
            contents << content_tag(:ul, error_messages)

            content_tag(:div, contents.html_safe, html)
          end
        else
          ''
        end
  end

  ####################
  #
  # added to make the errors display in a single line per field
  #
  ####################
  def full_flat_messages(object)
    full_messages = []

    object.errors.each_key do |attr|
      msg_part=msg=''
      object.errors[attr].each do |message|
        next unless message
        if attr == "base"
          full_messages << message
        else
          msg=object.class.human_attribute_name(attr)
          msg_part+= I18n.t('activerecord.errors.format.separator', :default => ' ') + (msg_part=="" ? '': ' & ' ) + message
        end
      end
      full_messages << "#{msg} #{msg_part}" if msg!=""
    end
    full_messages
  end

end
10
Nate Murray

[Update] Jan/2013 to Rails 3.2.x - syntaxe de mise à jour; ajouter des spec

Inspiré par les nouvelles méthodes validation de Rails 3.0, j'ajoute ce petit validateur. Je l'appelle ReduceValidator.

lib/reduce_validator.rb:

# show only one error message per field
#
class ReduceValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    return until record.errors.messages.has_key?(attribute)
    record.errors[attribute].slice!(-1) until record.errors[attribute].size <= 1
  end
end

Mon modèle ressemble à - remarquez le:reduce => true:

validates :title, :presence => true, :inclusion => { :in => %w[ Mr Mrs ] }, :reduce => true
validates :firstname, :presence => true, :length => { :within => 2..50 }, :format => { :without => /^\D{1}[.]/i }, :reduce => true
validates :lastname, :presence => true, :length => { :within => 2..50 }, :format => { :without => /^\D{1}[.]/i }, :reduce => true

Fonctionne comme un charme dans mon projet Rails actuel. L’avantage, c’est que j’ai mis le validateur sur quelques champs seulement.

spec/lib/reduce_validator_spec.rb:

require 'spec_helper'

describe ReduceValidator do

  let(:reduce_validator) { ReduceValidator.new({ :attributes => {} }) }

  let(:item) { mock_model("Item") }
  subject { item }

  before(:each) do
    item.errors.add(:name, "message one")
    item.errors.add(:name, "message two")
  end

  it { should have(2).error_on(:name) }

  it "should reduce error messages" do
    reduce_validator.validate_each(item, :name, '')
    should have(1).error_on(:name)
  end

end
34
rzar

Plus simple, c'est:

<% @model.errors.each do |attr, msg| %>
  <%= "#{attr} #{msg}" if @model.errors[attr].first == msg %> 
<% end %>
16
olownia

Qu'en est-il de ce @event.errors[:title].first

3
jawaica

J'ai écrit un assistant personnalisé

def display_error(field)
    if @user.errors[field].any?
        raw @user.errors[field].first+"<br>"
    end
end

puis je l'utilise en vue sous le champ de texte comme si

<%= display_error(:password) %>
3
Maciej Goszczycki

Similaire à la réponse de olovwia :

<% @errors.keys.each do |attr| %>
 <%= "#{attr.capitalize} #{@errors[attr].first}."%>
<% end %>"
2
Alex Popov

J'utilise ce code pour la version 3.0 de Ruby on Rails, que j'ai mise dans lib/core_ext/Rails/active_model/errors.rb:

module ActiveModel
  class Errors
    def full_message_per_field
      messages_per_field = []
      handled_attributes = []

      each do |attribute, messages|
        next if handled_attributes.include? attribute
        messages = Array.wrap(messages)
        next if messages.empty?

        if attribute == :base
          messages_per_field << messages.first
        else
          attr_name = attribute.to_s.gsub('.', '_').humanize
          attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
          options = { :default => "%{attribute} %{message}", :attribute => attr_name }

          messages_per_field << I18n.t(:"errors.format", options.merge(:message => messages.first))
        end

        handled_attributes << attribute
      end

      messages_per_field
    end
  end
end

Il s’agit essentiellement du même code que ActiveModel::Errors#full_messages, mais n’affichera pas plus d’une erreur par attribut. Assurez-vous d'exiger le fichier (par exemple, dans un initialiseur) et vous pouvez maintenant appeler @model.errors.full_message_per_field do |message| ...

2
Michelle Tilley

Je voudrais afficher tous les messages d'erreur sur une ligne et dans un format de phrase. Vous ne voulez pas que l'utilisateur corrige une erreur et finisse par avoir une autre erreur dont il n'était pas conscient après la soumission. Leur dire toutes les règles leur sauvera les clics. Cela dit, voici comment je le ferais:

flash_message_now("error", 
   @album.errors.keys.map { |k| "#{Album.human_attribute_name(k)} #{@album.errors[k].to_sentence}"}.to_sentence
)

avec flash_message_now défini dans ApplicationController (vous pouvez l'ajouter à un assistant)

def flash_message_now(type, text)
    flash.now[type] ||= []
    flash.now[type] << text
  end
1
Abdo

Ajouter une méthode à la classe ActiveModel :: Errors

module ActiveModel
  class Errors
    def full_unique_messages
      unique_messages = messages.map { |attribute, list_of_messages| [attribute, list_of_messages.first] }
      unique_messages.map { |attribute_message_pair| full_message *attribute_message_pair }
    end
  end
end

Ajoutez-le à un fichier, comme lib/core_ext/Rails/active_model/errors.rb. Créez un fichier config/initializers/core_ext.rb et ajoutez-y un require "core_ext/Rails/active_model/errors.rb".

1
Coffee Bite
# Extracts at most <strong>one error</strong> message <strong>per field</strong> from the errors-object.
# @param  [ActiveModel::Errors] the_errors_object The errors-object.
# @raise  [ArgumentError] If the given argument is not an instance of ActiveModel::Errors.
# @return [Array] A string-array containing at most one error message per field from the given errors-object.
def get_one_error_per_field(the_errors_object)
  if the_errors_object.is_a? ActiveModel::Errors    
    errors = {}  
    the_errors_object.each do |field_name, associated_error|
      errors[field_name] = the_errors_object.full_message(field_name, associated_error) unless errors[field_name]
    end 
    return errors.values
  else
    raise ArgumentError.new('The given argument isn\'t an instance of ActiveModel::Errors!')
  end 
end 
0
BobMcgregor

Ou vous pouvez simplement modifier le tableau (avec la méthode 'bang' delete_at), de sorte que tout reste après Rails, i18n, etc.

<% @article.errors.keys.each { |attr| @article.errors[attr].delete_at(1) } %> 

Code de travail complet:

<% if @article.errors.any? %>
  <% @article.errors.keys.each { |attr| @article.errors[attr].delete_at(1) } %> 
   <ul>
    <% @article.errors.full_messages.each do |msg| %>
     <li><%= msg %></li>
    <% end %>
  </ul>
<% end %>
0
Frank B

Mon patch de singe de ActiveModel::Errors class lib/core_ext/Rails/active_model/errors.rb (j'utilise ce code pour la version Ruby on Rails 5.0): 

module ActiveModel
  class Errors

    # don't add an attribute's error message to details
    # if it already contains at least one message

    alias_method :old_add, :add

    def add(attribute, message = :invalid, options = {})
      if details[attribute.to_sym].size.zero?
        old_add(attribute, message, options)
      end
    end

  end
end

Créez un fichier config/initializers/core_ext.rb et ajoutez-lui un require core_ext/Rails/active_model/errors.rb.

0
c80609a