Handling devise registration errors - ruby-on-rails

I've subclassed the devise RegistrationsController for creating new users and added some logic before calling the superclass's 'create' method. So, something like:
class RegistrationsController < Devise::RegistrationsController
def create
super end
I can tell if the superclass encountered an error by checking resource.errors.nil?. However, I want to distinguish between different errors. For instance, I want to do something different if the error is "Email has already been taken" versus some other error return.
I can parse the string, but that seems fragile to me. What if some future upgrade of ActiveRecord or Devise changes the string? What if the string get's localized in some way I don't expect?
Is anyone handling error processing in devise more gracefully than string parsing?

you can modify devise.en.yml for any default errors

Notice that the devise_error_messages helper is just running through the errors attached to whatever you have assigned as your resource object (whatever user model you ran the install generator on).
Now, instead of just printing out the error messages in the helper, you could access their keys in a controller method, as well:
# in RegistrationsController
def create
build_resource
unless resource.valid?
if resource.errors.has_key?(:my_error_key)
# do something
end
end
end
This is just an example, of course, but hopefully it illustrates the approach you might take.

With Rails validation errors the devil is in the #details.
A typical validation error on presence looks like this:
> #user.errors
#<ActiveModel::Errors:0x007fe7e8f01234 #base=#<User id: nil,
email: "someone#else.mail", created_at: nil, updated_at: nil>,
#messages={:password=>["can't be blank"], :email=>[]},
#details={:password=>[{:error=>:blank}]}>
As you see, the failed validation is accurately described in #details of the ActiveModel Error object.
There is even a short hand method in Rails that makes it easy to test for specific validation errors:
#user.errors.added? :password, :blank
If the password is left blank, this will return true.
More about added? in the Ruby on Rails API: http://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-added-3F

Related

Grab unsaved document from response received after server validation

I am using (have to) Rails 3.2.12 and ruby 1.9.3 with mongoid as orm
Maybe my question doesn't make sense but let me ask
I want to know the ID or any other attribute of the unsaved document which is not being validated.
Like in the following screenshot I want to grab the ID of the second document whose one attribute is invalid. This page was rendered because server validation didn't go through.
Though it's not entirely clear from your question what is causing validation to fail, the text "can't be blank" is usually coming from ActiveModel::Errors.
Whenever validation is failed on the model and the page is re-rendered, it passes a hash that looks like this (example for a class Item that fails :create because it's missing a name):
#<ActiveModel::Errors:0x00007fe63fcff710
#base=#<Item id: nil, name: nil, user_id: 15, created_at: nil, updated_at: nil>,
#messages={:name=>["can't be blank"]},
#details={:name=>[{:error=>:blank}]}>
Inside that hash you will have lots of useful information (including the "can't be blank error" that is showing in your example). You can grab those errors in a number of different ways, but often you do it with an instance variable in the controller. For example:
class WhateverController < ApplicationController
def create
#thing = Thing.new(thing_params)
if #thing.save
...do success stuff...
else
flash[:error] = #thing.errors.full_messages
render :new
end
end
end
Call Byebug (or similiar) at the top of the else statement and then you can have a look in the console what is in #thing.errors
If the record is not being created, you won't have an ID in the hash (because it hasn't been saved), but if it's an update action then it should appear in the errors hash.

Rails force to_param to return something even when not persisted

I need to handle a particular case of generating email views with URLs constructed from non-persisted data.
Example : assume my user can create posts, and that triggers a post creation notification email, I'd like to send the user an example of fake post creation. For this, I am using a FactoryGirl.build(:post) and passing this to my PostMailer.notify_of_creation(#post)
In everyday Rails life, we use the route url_helpers by passing as argument the model itself, and the route generator will automatically convert the model into its ID to be used for the route URL generation (in article_path(#article), the routes helper converts #article into #article.id for constructing the /articles/:id URL.
I believe it is the same in ActiveRecord, but anyways in Mongoid, this conversion fails if the model is not persisted (and this is somewhat nice as it prevents the generation of URLs that may not correspond to actual data)
So in my specific case, URL generation crashes as the model is not persisted:
<%= post_url(#post_not_persisted) %>
crashes with
ActionView::Template::Error: No route matches {:action=>"show", :controller=>"posts", :post_id=>#<Post _id: 59b3ea2aaba9cf202d4eecb6 ...
Is there a way I can bypass this limitation only in a very specific scope ? Otherwise I could replace all my resource_path(#model) by resource_path(#model.id.to_s) or better #model.class.name but this doesn't feel like the right situation...
EDIT :
The main problem is
Foo.new.to_param # => nil
# whereas
Foo.new.id.to_s # => "59b528e8aba9cf74ce5d06c0"
I need to force to_param to return the ID (or something else) even if the model is not persisted. Right now I'm looking at refinements to see if I can use a scoped monkeypatch but if you have better ideas please be my guest :-)
module ForceToParamToUseIdRefinement
refine Foo do
def to_param
self.class.name + 'ID'
end
end
end
However I seem to have a small scope problem when using my refinement, as this doesn't bubble up as expected to url_helpers. It works fine when using te refinement in the console though (Foo.new.to_param # => 59b528e8aba9cf74ce5d06c0)
I found a way using dynamic method override. I don't really like it but it gets the job done. I am basically monkeypatching the instances I use during my tests.
To make it easier, I have created a class method example_model_accessor that basically behaves like attr_accessor excepts that the setter patches the #to_param method of the object
def example_model_accessor(model_name)
attr_reader model_name
define_method(:"#{model_name}=") do |instance|
def instance.to_param
self.class.name + 'ID'
end
instance_variable_set(:"##{model_name}", instance)
end
end
Then in my code I can just use
class Testing
example_model_accessor :message
def generate_view_with_unpersisted_data
self.message = FactoryGirl.build(:message)
MessageMailer.created(message).deliver_now
end
end
# views/message_mailer/created.html.erb
...
<%= message_path(#message) %> <!-- Will work now and output "/messages/MessageID" ! -->

Use devise for a model other than user

So, I am somewhat new to rails and devise, so I apologize in advance if this is a basic question. I couldn't find any information on this anywhere, and I searched thoroughly. This also makes me wonder if Devise is the right tool for this, but here we go:
I have an app where devise user authentication works great, I got it, implemented it correctly and it works.
In my app, users can belong to a group, and this group has a password that a user must enter to 'join' the group.
I successfully added devise :database_authenticatable to my model, and when I create it an encrypted password is created.
My problem, is that I cannot authenticate this! I have a form where the user joins the group, searching for their group, then entering the password for it.
This is what I tried:
def join
#home = Home.find_for_authentication(params[:_id]) # method i found that devise uses
if #home.valid_password?(params[:password]);
render :json => {success: true}
else
render :json => {success: false, message: "Invalid password"}
end
end
This gives me the error: can't dup NilClass
on this line: #home = Home.find_for_authentication(params[:_id])
What is the problem?
The problem will be here:
Home.find_for_authentication(params[:_id])
I've never used database_authenticatable before (will research it, thanks!), so I checked the Devise docs for you
The method they recommend:
User.find(1).valid_password?('password123') # returns true/false
--
Object?
The method you've used has a doc:
Find first record based on conditions given (ie by the sign in form).
This method is always called during an authentication process but it
may be wrapped as well. For instance, database authenticatable
provides a find_for_database_authentication that wraps a call to
this method. This allows you to customize both database
authenticatable or the whole authenticate stack by customize
find_for_authentication.
Overwrite to add customized conditions, create a join, or maybe use a
namedscope to filter records while authenticating
The actual code looks like this:
def self.find_for_authentication(tainted_conditions)
find_first_by_auth_conditions(tainted_conditions)
end
Looking at this code, it seems to me passing a single param is not going to cut it. You'll either need an object (hence User.find([id])), or you'll need to send a series of params to the method
I then found this:
class User
def self.authenticate(username, password)
user = User.find_for_authentication(:username => username)
user.valid_password?(password) ? user : nil
end
end
I would recommend doing this:
#home = Home.find_for_authentication(id: params[:_id])
...

Display link in Rails form error message

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!

calling custom validation methods in Rails

I just upgraded my rails to 2.3.4 and I noticed this with validations:
Lets say I have a simple model Company which has a name. nothing to it.
I want to run my own validation:
class Company < ActiveRecord::Base
validate :something
def something
false
end
end
saving the model actually works in this case.
The same thing happens if i override validate() and return false.
I noticed this in a more complex model where my validation was returning false, but the object was still saving...I tried it out in an essentially empty model and the same thing applied. Is there a new practice I am missing? This doesn't seem to be the case in some of my older rails code.
Your validations are executed when you use the validate method. However rails doesn't relies on the returned value.
It relies on if there are validations errors or not. So you should add errors when your model doesn't validates.
def something
errors.add(:field, 'error message')
end
Or, if the error is not related to a field :
def something
errors.add(:base, 'error message')
end
Then your model won't be saved because there are errors.
You're getting confused between validations and callbacks.
Validations are supposed to fail if there are any errors on the object, doesn't matter what the validation returns. Callbacks fail if they return false, regardless if they add any errors to object.
Rails uses calls valid? from save calls which does not check the result of any validations.
Edit: Rails treats validate :method as a callback, but valid? still doesn't check for their results, only for errors they added to the object.
I don't think this behaviour changed at all but I could be wrong. I don't think I've ever written a validation to return false before.
Just FYI errors.add_to_base('error message') has been deprecated in rails 3 and got replaced by
errors[:base] << "Error message"
Or
errors.add(:base, "Error message")

Resources