Display link in Rails form error message - ruby-on-rails

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)

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

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.

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.

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.

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 :(

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!

Related

Rails validation is still firing despite unless option evaluating to true

I use devise_invitable in my app to allow users to send invitations. I realized a bad case in which a user has been invited but ignores the invitation and later returns to the app to sign up on their own. Because devise_invitable handles invitations by creating a new user using the provided email address for the invitation, my uniqueness validation on the email field will cause Rails to complain, telling the user that the email address is already taken.
I'm trying to write some logic to handle this case. I see two paths - either figure a way to detect this and destroy the previously created user and allow the new one to be created, or detect the user was invited and execute another flow. I've decided to implement the second option, as I'd like to still utilize the invitation if possible.
My limited experience has me questioning if what I've written will work, but I can't actually fully test it because the Rails validation on the email is triggered. I've made sure Devise's :validatable module is inactive. I created a method that (I think) will detect if a user was invited and in that case the uniqueness validation should be skipped.
#user.rb
...
validates :email, uniqueness: true, unless: :was_invited?
...
def was_invited?
if self.invitation_sent_at.present? && self.sign_in_count == 0
true
else
false
end
end
FWIW, I had originally written this in shorthand rather than breaking out the if/else, but I wanted to be very explicit in an effort to find the bug/failure.
The hope is that once the form passes validation, the create action will do some detection about a user's invitation status and, if they were invited, redirect them to the accept_user_invitation_path. Again, I haven't been able to actually test this yet because I can't get around the validations.
#registrations_controller.rb
def create
if User.find_by_email(params[:email])
#existing_user = User.find_by_email(params[:email])
#existing_user.save(validate: false)
if #existing_user.was_invited?
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
end
else
super
end
end
In a desperate effort, you'll see I've also added the .save(validate: false) to try to short circuit it there, but it's not even getting that far.
If I comment out the email validation entirely, simply to test the rest of the logic/flow, I get a PG error complaining on uniqueness because of an index on the email address - I don't want to tear all this apart simply to test this method.
I've tried to mess with this for hours and I'm at a loss - any help is appreciated. Let me know if there's any other code you want to see.
Looking at the redirect:
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
I can see that there is no return which should mean that if that redirect was being called you should be getting an AbstractController::DoubleRenderError error as the parent controller's create method should be trying to render the new view.
From this I would guess that the query you are using to find the existing user is not actually returning a result, possibly because you are using params[:email] whereas if you are using the default views or a properly formatted form it should be params[:user][:email].
Maybe you should give more responsibilities to your controller...
If you find the user, use that, else create a new one. Assuming your form appears with http://yourapp/users/new, change it in your routes to http://yourapp/users/new/:email, making the user input their email before advancing to the form.
def new
#existing_user = User.find_by_email("#{params[:email]}.#{params[:format]}") || User.new
if #existing_user.was_invited? # will only work for existing user
redirect_to accept_user_invitation_path(:invitation_token => #existing_user.invitation_token)
else
render 'new'
end
end
def create
# do maybe something before saving
if #existing_user.save(user_params)
# do your magic
else
render 'new', notice: "Oops, I didn't save"
end
end

How to stop a user form adding forms to a field?

I have a form that allows a user to update their profile information, but I would like to prevent some information from being changed. I also would like to keep my controller code very simple. In the update action of my Users Controller, I have the following code:
def update
#user = Users.find params[:id]
if #user.update_attributes(params[:user])
flash[:notice] = 'Update successful.'
redirect_to user_path(#user)
else
render :action => :edit
end
end
This is very clean and simple, and I like that. What I don't like, however, is that a user can add a field to the form, with the same name as an attribute, and use it to modify forbidden attributes. Is there a simple way to do this, or do I need to devise a way to do this myself?
One method I was considering was to generate a hash value, using a hash-based message authentication code, of all the form's element names. This message access code would be a hidden value in the form. Then, once the form is submitted, I would calculate the message access code (MAC) again using the names of the parameter Hash's keys. If the two MACs are different, or if the first MAC is missing from the parameter Hash, I would throw an error. I would rather not spend the time implementing this if there was already and easy solution out there.
Thanks.
On your model you can use attr_protected or attr_accessible to blacklist or whitelist attributes when being set via mass assignment (like when a form is submitted).
Rails will prevent mass assignment if you use attr_protected :protectedcolumn (blacklist) or attr_accessible :safecolumn (whitelist) within your model. More information on this topic can be found in the Ruby on Rails Security Guide (Section 6.1)

Soft db error handling for duplicate entries? -- Rails 3.1 newbie

If a user tries to enter a duplicate entry into a table, they get a full page nasty error
ActiveRecord::RecordNotUnique in Admin::MerchantsController#add_alias
Mysql2::Error: Duplicate entry '2-a' for key 'merchant_id': ..
Is there a way to give a flash message instead like "This record already exists"?
This thread from railsforum can help you
I wouldn't recommend checking for the uniqueness and specifically responding to this validation rule via a flash message, this conventionally should be handled by the form's error messages instead.
Nonetheless, in the controller actions for creating and updated you could wrap the conditional block which decides where to send the user, with a check for uniqueness first:
def create
#merchant = Merchant.new params[:merchant]
if Merchant.where(:merchant_id => #merchant.id).count > 0
flash[:error] = "The merchant id #{#merchant.id} already exists"
render :create # amend this to :update when applying to the update action
else
# your normal generated save and render block
end
end
This isn't the cleanest way of achieving your goal, but I think it'll be easiest to understand.
Would really recommend the model validations and form error messages instead, which if you are usung the generated scaffolding, all you need to do is add a model validation and the form throw out the error messages to the user for you:
# app/models/merchant.rb
class Merchant < ActiveRecord::Base
validates_uniqueness_of :merchant_id
end

Rails 3 translations within models in production

I'm trying to translate an app into Japanese and everything was going smoothly until I put it into production.
As cache_classes is now true any translation within a model reverts to the default locale.
I know I'm probably supposed to target the translations directly in the yml file but I'm not sure how I would do that for the following simplified code:
class TimeseriesForecast < ActiveRecord::Base
##field_names = {
:location_name => I18n.t('forecast_timeseries.location_name'),
:local_date_time => I18n.t('forecast_timeseries.local_date_time'),
:zulu_date_time => I18n.t('forecast_timeseries.zulu_date_time'),
:temp_mean => I18n.t('forecast_timeseries.temp_mean')
}
end
Many thanks
Your I18n.t() call is evaluated at compile time since you are defining class variables, not instance variables. You need to put your call to I18n.t where they will be evaluated at runtime.
But if you want to translate ActiveRecord field names, use human_attribute_name and provide your translations via YML. You do not need to manually provide translations, Rails handles it all for you automatically.
The respective documentation is at http://guides.rubyonrails.org/i18n.html Chapter 5.1.
Don't use I18n.t or translate method in your models. You can do this instead:
In your model
Use something like this to add internationalized errors to the name attribute of your model (Check documentation: ActiveModel/Errors/method-i-add):
self.errors.add(:name, :your_error_key)
# The error key could be something like :wrong_name
NOTE: Sometimes you won't even need to add errors with errors.add method. For example if you add validations in your model with somethind like this:
validates :name, presence: true
Rails will add an error with the key :blank (the key depens on the validation type). In other words rails internally will issue self.errors.add(:name, :blank)
In your locale
Then in your locale.jp.yml can use any of this (just one):
activerecord.errors.models.[model_name].attributes.[attribute_name]
activerecord.errors.models.[model_name]
activerecord.errors.messages
errors.attributes.[attribute_name]
errors.messages
In your case replace [model_name] with timeseries_forecast and [attribute_name] with your_error_key
For example:
en:
errors:
messages:
your_error_key: "Your error message in english"
Don't think you're improving performance by caching the names in the class. Make it a method instead.
class TimeseriesForecast < ActiveRecord::Base
def self.field_names
{ :location_name => I18n.t('forecast_timeseries.location_name'),
:local_date_time => I18n.t('forecast_timeseries.local_date_time'),
:zulu_date_time => I18n.t('forecast_timeseries.zulu_date_time'),
:temp_mean => I18n.t('forecast_timeseries.temp_mean') }
end
end
# usage
TimeseriesForecast.field_names
Better yet, return just the actual fields and do the translation in the view, if you're gonna be strict MVC about it (some Rails methods - like collection_select - make it harder to do that though, hence the suggestion above).

Friendly Form Validations (Rails)

I checked out both of these previously-asked questions, and they're a help but not a full solution for my case.
Essentially I need to validate a user-submitted URL from a form. I've started by validating that it begins with http://, https://, or ftp:// :
class Link < ActiveRecord::Base
validates_format_of [:link1, :link2, :link3,
:link4, :link5], :with => /^(http|https|ftp):\/\/.*/
end
That works great for what it's doing, but I need to go these two steps further:
Users should be allowed to leave the form fields blank if needed, and
If the URL provided by the user does not already start with http:// (say they enter google.com, for example), it should pass the validation but add the http:// prefix while being processed.
I'm having a hard time determining how to make this work cleanly and efficiently.
FYI, you don't have to pass an array to validates_format_of. Ruby will do arrays automagically (Rails parses the output of *args).
So, for your question, I'd go for something like this:
class Link < ActiveRecord::Base
validate :proper_link_format
private
def proper_link_format
[:link1, :link2, :link3, :link4, :link5].each do |attribute|
case self[attribute]
when nil, "", /^(http|https|ftp):\/\//
# Allow nil/blank. If it starts with http/https/ftp, pass it through also.
break
else
# Append http
self[attribute] = "http://#{self[attribute]}"
end
end
end
end
Just to add to the above, I use the Ruby URI module to parse URLs for validity.
http://www.ruby-doc.org/stdlib/libdoc/uri/rdoc/classes/URI.html
It works really well and it helps me to avoid regexes.

Resources