13

On our sign-up form, we validates_uniqueness_of :email

When the a user is attempting to use our sign up form and they specify an existing email address, I'd like them to see an error message like this

This email address is already in use. If you're having trouble logging in, you can reset your password

Obviously, I'd like to use the named route for the link, but my User model does not have access to it. How can I accomplish this?

Side note: We will be offering translations for our app soon and all of these error messages will end up in YAML files. Can I somehow inject my new_password_url in a message in my YAML locale files? (e.g., config/locales/en.yml)

maček
  • 76,434
  • 37
  • 167
  • 198

6 Answers6

16

I know this is an old question, but for future users who want to insert a link into an error message, here are some guidelines that worked for me.

First, the I18n error messages are assumed html safe already, so you can go ahead and write a suitable error message. In this example, I'm changing an "email is taken" message.

# config/locales/en.yml
activerecord:
  errors:
    models:
      user:
        attributes:
          email:
            taken: 'has already been taken. If this is your email address, try <a href="%{link}">logging in</a> instead.'

Notice the interpolated variable %link.

Now all you need to is pass in a value for that variable in your validator, like so:

# app/models/user.rb
validates :email, :uniqueness => {:link => Rails.application.routes.url_helpers.login_path}

(By default, any options you pass in here will automatically be sent over to the I18n translator as variables, including some special pre-populated variables like %value, %model, etc.)

That's it! You now have a link in your error message.

Elliot Nelson
  • 11,371
  • 3
  • 30
  • 44
  • 1
    I've always wondered how to access the url/path helpers like that. Thank you so much :) – maček Sep 26 '12 at 17:59
  • Full list of translations available at https://github.com/svenfuchs/rails-i18n/blob/f8606e62def45279f3498549f97699049135bd11/rails/locale/en-US.yml] Also, if you are running a custom validation and want to it to register as uniqueness, you can pass in :taken, just like the link: `user.errors.add(:email, :taken, :link)` – d3vkit Mar 28 '13 at 21:05
  • clarification on @d3vkit's comment, last argument must be a hash, not a symbol. eg: `user.errors.add(:email, :taken, :link => '/some/path')` – Jon Garvin Aug 09 '13 at 19:07
  • 1
    The message appears as text / uninterpreted html. Any thoughts on how to output html from an ActiveRecord error message? – csi Jun 22 '15 at 22:18
  • @csi you need to call .html_safe on the message where you render it – edwardmp Feb 14 '17 at 22:55
  • @JonGarvin Just tried what you said and it didnt work. – Mark Jan 02 '18 at 14:39
1

This may not streamline well with the translations, but here's a suggestion:

In your user_controller#create action, wrap everything you already have with an if statement. Here's a rough example:

class UserController < ApplicationController
...

def create
  if User.find(params[:email])
    flash[:alert] = "This email address is in use.  You can ".concat(generate_reset_password_link(params[:email])
    render :action => 'new'
  else
    <your current code>
  end
end

After this, you'll have to write a helper method generate_reset_password_link, but I think this mostly respects the MVC layout. The controller is meant to interface with the view and model. It is violating the DRY principle a little, since you're essentially bypassing validates_uniqueness_of :email, but you get some custom behavior. DRY doesn't seem to be 100% achievable to me if you want to make more complex apps, but perhaps you can refine this and prove me wrong ;)

You may have to massage this a little so that the render :action => 'new' will repopulate itself with the previously entered data (in case the user just mistyped his own email address and it actually isn't in the system).

If you decide to use this approach, I would throw a comment in both the controller and the model indicating that the email uniqueness is essentially checked in 2 places. In the event someone else has to look at this code, it'll help them to understand and maintain it.

Eric Hu
  • 18,048
  • 9
  • 51
  • 67
  • I hate to say it, but this doesn't really feel like an elegant solution either. I'd like error to be specifically associated with the `:email` field on the `User` model. A generic `flash[:alert]` could work, but I'd rather not display two errors that occur one one field in separate locations on the page. – maček Mar 22 '11 at 18:43
  • I don't see how you'd generate two errors. You wouldn't see the validation error--you only see that when you attempt to save. Here, you'd would see an error specifically associated with that email. That's what the `generate_reset_password_link(params[:email])` would do. You give it the email address that the user has provided and it gives you a link to reset the password for the user with that specific email address. If you don't want to have this in the flash section, you could pass it as a param to the view and have it display where you please. – Eric Hu Mar 22 '11 at 19:39
0

You can place a tag of your own like ~[new_password_url] in your error messages. Then at the point of rendering your error messages gsub ur tag with the actual. if you want to do it generically you can get the path out using regexp and then eval it to get the url then gsub it back in. make you use the raw method if you are putting html into your text.

Kalendae
  • 2,256
  • 1
  • 21
  • 23
  • 1
    sorry to be crass, but this suggestion just made my skin crawl. – maček Mar 17 '11 at 20:38
  • 1
    but you are looking to break the convention of mvc by having knowledge of how to create a url link in the model. something's got to give. you can alternatively pass your request objects to your models or even store it in your thread class but to me that is more 'skin crawling' then having a replacement tag. – Kalendae Mar 21 '11 at 21:20
  • I don't normally participate in internet conflicts, but I just received 5 downvotes on 5 completely unrelated questions that lines up too perfectly with your 5 downvotes for today. This is some extremely childish behavior. – maček Mar 22 '11 at 18:48
  • 4
    Kalendae, kudos for having those votes reverted. Just to be sure @macek knows too, see [here](http://meta.stackexchange.com/questions/84330/how-to-undo-downvotes-or-report-self-for-abuse). Now: peace! – Arjan Mar 23 '11 at 10:59
0

If you're using 2.3.x, replace your call to error_messages with your own helper, written in UsersHelper. It should accept the FormBuilder or an ActiveRecord object and adjust the error message as you see fit. You could make as many customizations as you like, or it could be as simple as a gsub:

def user_error_messages(f)
  find_error = "This email address is already in use."
  replacement = "This email address is already in use. #{link_to(...)} to reset your password"
  f.error_messages.sub(find_error, replacement).html_safe
end

If you're using Rails3, make a helper method to simply process @user.errors.full_messages before they're emitted to the view.

John Douthat
  • 40,711
  • 10
  • 69
  • 66
0

Stumbled across this today:

http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html

If you need to access this auto-generated method from other places (such as a model), then you can do that by including ActionController::UrlFor in your class:


Step 1

Getting awareness of named routes to the model is the hard part; this gets me most of the way.

Now I can do something along the lines of

class User < ActiveRecord::Base
  include Rails.application.routes.url_helpers

  def reset_password_uri
    new_user_password_path(self)
  end
end

# User.find(1).reset_password_uri => "/users/password/new"

Step 2

So we have access to the named route, but now we need to inject it into the YAML message.

Here's what I just learned about YAML variables:

# en.yml
en:
  welcome: "Hello, %{username}!"

# es.yml
es:
  welcome: "¡Hola, %{username}!"

I can inject the username by including a hash with the t method

<div id="welcome">
  <%= t :welcome, :username => @user.username %>
</div>

Step 3

Now we just need a way to add interpolation to the error message described in the original question. This is where I am currently stuck :(

maček
  • 76,434
  • 37
  • 167
  • 198
-1

After hours trying to figure this out for Rails 4 with devise, I realised you can just add the link directly into the validation:

# app/models/user.rb
validates :username, presence: true, uniqueness: {:message => "username has already been taken - <a href='/users'>search users</a>" }

where the link in this case is my users index. Hopefully this will help someone!

rubynewbie14
  • 109
  • 1
  • 5
  • 1
    You've mixed a HTML, a static route, and localized text in your model validation code. – maček May 25 '15 at 15:17
  • Thanks for this - still new to the "right" way to do things. I couldn't get it to work any other way. Would welcome any thoughts on alternative solution, amending en.yml didn't work for me. – rubynewbie14 Jun 04 '15 at 07:59