Skipping Validation for custom fields added to Devise user model - ruby-on-rails

I've tried to research this but oddly no one else seems to have this problem or I am not searching right. Anyway here is my user.rb. I can create a user no problem and my custom field, 'pin, gets set. I can update the 'pin' field. But if try to change name, email or password they fail because the pin is blank per the validation in my model. How to skip the pin validation if I am not updating it? Also, if I update my email, it still sends a confirmation but when I click on the link it validates the pin again and the confirmation fails. Also, I don't want to regenerate the hash as well in the after_validation helper.
class User < ApplicationRecord
has_many :profiles, dependent: :destroy
has_many :general_settings, dependent: :destroy
has_many :profile_settings, through: :profiles
has_many :tags, dependent: :destroy
has_many :videos, through: :profiles
belongs_to :account_type
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable
after_validation :create_parent_digest
validates :pin, presence: true, length:{ is: 4 }, confirmation: true
attr_accessor :pin, :pin_confirmation
def valid_pin?(pin)
Devise::Encryptor.compare(self.class, parent_digest, pin)
end
private
def create_parent_digest
self.parent_digest = Devise::Encryptor.digest(self.class, pin)
end
end

According to the Rails Guide, you can skip validations on save by adding `validate: false as on option. For example:
#user.save(validate: false)

You should use option on: http://guides.rubyonrails.org/active_record_validations.html#on
For example:
validates :pin, presence: true, length:{ is: 4 }, confirmation: true, on: :create
or you can create a custom context for validation
# in model
validates :pin, presence: true, length:{ is: 4 }, confirmation: true, on: :validate_pin
# in controller
#user.save(context: :validate_pin)
I recommend to do not use callbacks for creating something, just make create_parent_digest a public method and call it by hand in controller(or service object)
# UsersController
if #user.save
#user.create_parent_digest
...
end
Edit: you can do like this
# in model
validates :pin, presence: true, length:{ is: 4 }, confirmation: true, on: :validate_pin
# in controller
def create
#user = User.new params
if #user.valid?(:validate_pin)
#user.create_parent_digest
#user.save
else
# do something like render :new
end
end

Related

Devise: Authenticate with Just Username Only Instead of Email

everyone!
So I'm building an application that only needs a username for authentication and not an email.
In my config/initializers/devise.rb, I changed the :email to :username resulting to this
config.authentication_keys = [:username]
config.case_insensitive_keys = [:username]
config.strip_whitespace_keys = [:username]
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :stores
has_many :customers, through: :stores
has_many :visits, through: :stores
has_many :mall_managers
validates :name, presence: true
validates :username, presence: true, uniqueness: true
end
I also edited the views accordingly.
When I try to sign up I get an error:
NoMethodError in Devise::RegistrationsController#create
undefined method `email' for #<User:0x000000011bd61b38>
i guess your user table does not contain email column, but the module validatable of gem devise will validates_presence_of the email, so that error will be raised.
however module validatable allow you ignore email validation by override email_required? method
class User < ApplicationRecord
# ...
def email_required?
false
end
def will_save_change_to_email?
false
end
def email_changed?
false
end
end
update i found more 2 methods need to override (as above), and who know what else (in future versions) ? so i recommend you add email database field (if the root cause i suspect (miss email) correct).

PG::UndefinedColumn: ERROR: column courts.user_id does not exist

This is the error I receive when I run a test for this file.
The error is in line 83 when I attempt to remove the user from the database using the 'destroy' method.
This is the repo in question frozen in time.
The error:
ActiveRecord::StatementInvalid:
PG::UndefinedColumn: ERROR: column courts.user_id does not exist
LINE 1: SELECT "courts".* FROM "courts" WHERE "courts"."user_id" = $...
I'm trying to connect the table users with the table court this way:
User is the administrator of many courts
A court can have only one administrator
As you can see, 'Court' has a foreign key linked to 'users', but called 'administrator'.
I want 'administrator' to be the alias of a user because in the future User and Court might have more relationships: 'owner' for example.
I can see that active record is building this query:
SELECT "courts".* FROM "courts" WHERE "courts"."user_id" = $...
Maybe this can be solved if Active Record build a query this way
SELECT "courts".* FROM "courts" WHERE "courts"."administrator_id" = $...
But I don't know how to do it and if that would be prudent.
Maybe there is another way to do this. Something tidier, I feel I'm not doing the association properly.
What do you recommend?
models/user.rb
class User < ActiveRecord::Base
before_save :format_input
# extend Devise::Models
# Include default devise modules .
devise :database_authenticatable,
# :validatable,
# :recoverable,
# :rememberable,
# :trackable,
# :confirmable,
# :omniauthable,
:registerable
# note that this include statement comes AFTER the devise block above
include DeviseTokenAuth::Concerns::User
validates :first_name, presence: true, length: { in: 1..20 }
validates :last_name, presence: true, length: { in: 1..20 }
validates :email, uniqueness: true
validates_format_of :email, with: /#/
validates :password, presence: true, length: { in: 8..20 }, :on => :create
has_many :courts, dependent: :destroy
private
def format_input
self.first_name = first_name.downcase.titleize
self.last_name = last_name.downcase.titleize
self.email = email.downcase
end
end
models/court.rb
class Court < ApplicationRecord
belongs_to :administrator, :class_name => :user
validates :name, presence: true, length: { in: 1..20 }
validates :address, presence: true, length: { in: 1..50 }
validates :description, presence: true, length: { in: 1..100 }
validates :description, presence: true, length: { in: 1..100 }
end
The issue is on this line.
has_many :courts, dependent: :destroy
By default, ActiveRecord assumes the foreign key is named <lowercased_parent_class_name>_id. In this case, when you delete a user, ActiveRecord is trying to delete the associated courts using the user_id foreign key which doesn't exist. Pass the foreign_key option to the has_many call.
has_many :courts, dependent: :destroy, foreign_key: :administrator_id
From the docs:
:foreign_key
Specify the foreign key used for the association. By default this is guessed to be the name of this class in lower-case and _id suffixed. So a Person class that makes a has_many association will use person_id as the default :foreign_key.
There's another issue. You should pass the uppercased class name as a string in the belongs_to call in the Court model.
belongs_to :administrator, class_name: 'User'
This is unrelated to the question but you might want to check if it is okay to delete a court when the user record is deleted.

Rails: undefined method `primary_key' for ActiveSupport::HashWithIndifferentAccess:Class

I have a polymorphic relationship for my User model. I am using Devise.
When I try to edit the user's details, I get the following error:
undefined method `primary_key' for ActiveSupport::HashWithIndifferentAccess:Class
The data submitted through the form is:
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"Ap0JP2bs/w9J6iI9rZahiKR1K8UEIi7rp33a4OutMbo=",
"user"=>{"email"=>"some_email#yahoo.com",
"rolable"=>{"first_name"=>"Christopher",
"last_name"=>"Columbus",
"city"=>"16"}},
"commit"=>"Update"}
The controller method is:
def update
#user = User.find(current_user.id)
if #user.update_attributes(params[:user])
redirect_to edit_user_registration_path, notice: 'Your profile was successfully updated.'
else
redirect_to edit_user_registration_path, error: "Something went wrong, couldn't update your profile!."
end
end
The models:
1. User
class User < ActiveRecord::Base
belongs_to :rolable, :polymorphic => true
# Devise business
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :remote_avatar_url, :avatar, :login, :username, :email, :password, :password_confirmation, :remember_me
# For authenticating with facebook
attr_accessible :provider, :uid, :rolable
devise :omniauthable, :omniauth_providers => [:facebook]
# For authenticating with both username or email
attr_accessor :login
# Upload avatar
mount_uploader :avatar, AvatarUploader
# Makes username necessary
validates :username, :presence => true
end
2. Customer
class Customer < ActiveRecord::Base
has_one :user, :as => :rolable
has_one :preferences, :class_name => "CustomerPreferences"
belongs_to :city
attr_accessible :first_name, :last_name
end
What's the problem?
Based on your request hash, you are passing the rolable attribute as:
"rolable"=>{"first_name"=>"Cristian",
"last_name"=>"Gavrila",
"city"=>"16"}
You can't do this unless you specify that the User model accepts nested attributes for rolable. However, you have setup your User as belonging to a Rolable rather than the other way around. Nested attributes aren't designed to handle this inverse relationship - you may want to reconsider what you are trying to accomplish here, and modify the relationship accordingly.
For instance, you may want to turn your hash inside out and pass the rolable attributes with the user attributes embedded within. Or you may want to turn rolable into a has_one association.

devise invitable do not validate model

First, excuse my poor english, I'm french... it's tricky to explain my problem !
I have a model User model in a Rails application:
class User < ActiveRecord::Base
attr_accessible :email, :gender, :lastname, :firstname
end
And a BackUser model that inherit from User:
class BackUser < User
# Class for Backoffice User
devise :database_authenticatable,
:rememberable,
:trackable,
:lockable,
:invitable,
:confirmable,
:validatable,
:validate_on_invite => true
attr_accessible :password, :password_confirmation, :remember_me, :active, :role
validates :role, presence: true,
inclusion: ["admin", "normal"]
validates :gender, presence: true
validates :firstname, presence: true
validates :lastname, presence: true
def admin?
self.role == 'admin'
end
end
This second class should validate the record before invite!
BUT, when I use the console to do the following:
u = BackUser.new
u.invite!
"u" is saved in database and an invitation is send to a blank email...
Do you know what I have to do?
Thans a lot!
I'm sure you've found a solution or workaround to your problem by now, but for any future SO users who encounter the same problem I found a pretty simple fix.
Devise Invitable's model configuration docs don't fully explain how to implement :validate_on_invite, but you have to set the configuration option to true - :validate_on_invite => true.
Here's what my devise method looks like in my User model for this to work correctly.
models/user.rb
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :validate_on_invite => true
Now when I attempt to submit an invitation it is fully validating the record with what the validations I've set up in my User model before allowing the invitation to be sent and the user record to be created. Looking at the docs, I'm guessing you can also enable this setting in the devise initializer, but I haven't tried going that route.
*Second possible option to enable validation if needed
config/initializers/devise.rb
config.validate_on_invite = true
I've never been able to get the validation to work correctly for devise invitable. You can't use RobHeaton's suggestion either because you will receive a validation error on the password. I use this little hack to get validation to work:
def create
#user = User.new(user_params)
#user.valid?
#user.errors.messages.except!(:password) #remove password from errors
if (#user.errors.any?)
render 'new'
else
#user.invite!(current_user)
redirect_to user_path(#user)
end
end
It doesn't solve the mystery of why your behaviour is occurring, but:
if u.save
u.invite!
end
will give the end result you are after.

ActiveRecord validations not working with custom attribute assignment method

I'm working on a Rails app with two-factor authentication. The User model in this app has an attribute, two_factor_phone_number. I have the model validating that this attribute is present before the model can be saved.
To make sure the phone number is saved in a proper format, I've created a custom attribute assignment method that looks like this:
def two_factor_phone_number=(num)
num.gsub!(/\D/, '') if num.is_a?(String)
self[:two_factor_phone_number] = num.to_i
end
I'm doing some acceptance testing, and I've discovered that if this method is in the model, the ActiveRecord validation is ignored/skipped and a new model can be created without a two_factor_phone_number set.
The model code looks like this:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable,
:lockable
attr_accessible :email, :password, :password_confirmation, :remember_me,
:first_name, :last_name, :two_factor_phone_number
validates :first_name, presence: true
validates :last_name, presence: true
validates :two_factor_phone_number, presence: true
# Removes all non-digit characters from a phone number and saves it
#
# num - the number to be saved
#
# Returns the digit-only phone number
def two_factor_phone_number=(num)
num.gsub!(/\D/, '') if num.is_a?(String)
self[:two_factor_phone_number] = num.to_i
end
end
You could add a format validation:
validates :two_factor_phone_number, :format => { :with => /[0-9]/,
:message => "Only digits allowed" }
and/or create another method to set this attribute and call it before validation
before_validation :update_phone_format
def update_phone_format
...
end

Resources