I have a Company model with attr_accessor :administrator, so when user creates company, he also need to fill some fields for administrator of this company. I'm trying to test, that he fill all fields correctly.
class Company < ActiveRecord::Base
attr_accessor :administrator
validates :name, presence: true
validates :administrator, presence: true, if: :administrator_is_valid?
private
def administrator_is_valid?
administrator[:name].present? and
administrator[:phone].present? and
administrator[:email].present? and
administrator[:password].present? and
administrator[:password_confirmation].present? and
administrator[:password] == administrator[:password_confirmation]
end
end
company_spec.rb is:
require 'rails_helper'
describe Company do
it 'is valid with name and administrator' do
company = Company.new(name: 'Company',
administrator: {
name: nil,
email: nil,
phone: nil,
password: 'password',
password_confirmation: ''
})
expect(company).to be_valid
end
end
So, as you see, I have a lot of mistakes in validation test, but RSpec pass it.
Thanks!
That's because you didn't construct your validation properly. See, if: administrator_is_valid? will return false for your test, telling Rails to skip this validation rule.
I suggest you drop using the presence validator in favor of using administrator_is_valid? method as a validation method, because after all, if the administrator is valid then it is present. The code should look like this
validate :administrator_is_valid?
private
def administrator_is_valid?
(administrator[:name].present? and
administrator[:phone].present? and
administrator[:email].present? and
administrator[:password].present? and
administrator[:password_confirmation].present? and
administrator[:password] == administrator[:password_confirmation]) or
errors.add(:administrator, 'is not valid')
end
You could clean up your code like this:
validate :administrator_is_valid?
private
def administrator_is_valid?
if administrator_cols_present? && administrator_passwords_match?
true
else
errors.add(:administrator, 'is not valid')
end
end
def administrator_cols_present?
%w(name phone email password password_confirmation).all? do |col|
administrator[col.to_sym].present? # or use %i() instead of to_sym
end
end
def administrator_passwords_match?
administrator[:password] == administrator[:password_confirmation]
end
Another improvement might be to move your administrator to a struct, then call valid? on the object.
admin = Struct.new(cols) do
def valid?
cols_present? && passwords_match?
end
def cols_present?
cols.values.all? { |col| col.present? }
end
def passwords_match?
cols[:password] == cols[:password_confirmation]
end
end
Then:
validate :administrator_is_valid?
def admin_struct
#admin_struct ||= admin.new(administrator)
end
def administrator_is_valid?
errors.add(:administrator, 'is not valid') unless admin_struct.valid?
end
Related
I am using devise for authentication. I am overwriting devise token generator so that I can use 6 digit code and also overwriting it so that I can support mobile number confirmation.
If a user register with email and OTP is send via email. Registration seems to work fine. A user register with an email. An OTP is sent and after confirmation a user gets confirmed.
But when the user tries to update the email. I am using the same methods to send the confirmation code (as in registration which works fine) the user get saved in unconfirmed_email. A mail gets send in email but after confirmation a user email is not being copied to email field from unconfirmed_email field.
What could be the problem here.
app/services/users/confirmation_code_sender.rb
# frozen_string_literal: true
module Users
class ConfirmationCodeSender
attr_reader :user
def initialize(id:)
#user = User.find(id)
end
# rubocop :disable Metrics/AbcSize
def call
generate_confirmation_token!
if user.email?
DeviseMailer.confirmation_instructions(
user,
user.confirmation_token,
{ to: user.unconfirmed_email || user.email }
).deliver_now
else
Telco::Web::Sms.send_text(recipient: user.unconfirmed_mobile || user.mobile_number, message: sms_text)
end
end
# rubocop :enable Metrics/AbcSize
private
def generate_confirmation_token!
user.confirmation_token = TokenGenerator.token(6)
user.confirmation_sent_at = DateTime.current
user.save!(validate: false)
end
def sms_text
I18n.t('sms.confirmation_token', token: user.confirmation_token)
end
end
end
app/services/users/phone_or_email_updater.rb
# frozen_string_literal: true
module Users
class PhoneOrEmailUpdater < BaseService
def call
authorize!(current_user, to: :user?)
current_user.tap do |user|
user.update!(unconfirmed_mobile: params[:unconfirmed_mobile], unconfirmed_email: params[:unconfirmed_email])
ConfirmationCodeSender.new(id: user.id).call
end
end
end
end
config/nitializers/confirmable.rb
# frozen_string_literal: true
# Overriding this model to support the confirmation for mobile number as well
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
if unconfirmed_email.present?
self.email = unconfirmed_email
self.unconfirmed_email = nil
true
elsif unconfirmed_mobile.present?
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
true
else
false
end
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end
Mobile update seems to be working fine but email is not updating. I am using graphql to update the email
In console I tried using .confirm but it seems to be not working as well the user email is not getting confirmed
In your pending_reconfirmation?, self.unconfirmed_email is assigned to be nil. It seems like pending_reconfirmation? is only called in saved, however, it is called by pending_any_confirmation, too.
https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L238
# Checks whether the record requires any confirmation.
def pending_any_confirmation
if (!confirmed? || pending_reconfirmation?)
yield
else
self.errors.add(:email, :already_confirmed)
false
end
end
So when the second time the pending_reconfirmation? is called in the saved, pending_reconfirmation? will return false because unconfirmed_email is nil.
You'd better not do actual assignments inside the methods end with ? it will be an implicit side-effect. Maybe create a new method end with ! to change the value of attributes.
For example:
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
reconfirm_email! if unconfirmed_email.present?
reconfirm_mobile! if unconfirmed_mobile.present?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
unconfirmed_email.present? || nconfirmed_mobile.present?
end
def reconfirm_email!
self.email = unconfirmed_email
self.unconfirmed_email = nil
end
def reconfirm_mobile!
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end
I want the password should be mandatory for web registration only and not for mobile app registration.
My validation code is below:
class User < ApplicationRecord
validate :password_for_web
validate :password_confirmation_for_web
def password_for_web
if !app_user && password.blank?
errors.add(:password, "can't be blank.")
end
end
def password_confirmation_for_web
if !app_user && password != self.password_confirmation=
errors.add(:password_confirmation, "doesn't match.")
end
end
end
Validation is working properly but when in case of mobile app registration it is still requiring password. Help regarding the issue would be appreciable.
You can use the if: and unless: options to toggle validations:
class User < ApplicationRecord
validates :password, presence: true, confirmation: true, unless: :app_user?
# or
validates :password, presence: true, confirmation: true, if: :web_user?
# TODO implement app_user?
end
You can pass a symbol (a method name) or a lambda.
If you are using simple rails app with responsive design.
You need to first check device is mobile or other device. This you can do in many ways.
Custom Way :
In application_helper.rb:
def mobile_device
agent = request.user_agent
return "tablet" if agent =~ /(tablet|ipad)|(android(?!.*mobile))/i
return "mobile" if agent =~ /Mobile/
return "desktop"
end
Then you can use it in your views:
<% if mobile_device == "mobile" %>
//add extra parameter to check in model
<% form_tag :mobile_device, true %>
<% end %>
In your model :
class User < ApplicationRecord
validate :password_for_web, if: :mobile_device?
validate :password_confirmation_for_web, if: :mobile_device?
def password_for_web
if !app_user && password.blank?
errors.add(:password, "can't be blank.")
end
end
def password_confirmation_for_web
if !app_user && password != self.password_confirmation=
errors.add(:password_confirmation, "doesn't match.")
end
end
def mobile_device?
mobile_device.present?
end
end
You can also use gems to check mobile device like:
https://github.com/fnando/browser
https://github.com/shenoudab/active_device
If you have separate mobile app.
Add extra parameter in your registration form for mobile application like I used in my view with name mobile_device. Use updated model code and you are done.
Calling validate :method_name causes an infinite loop SystemStackError.
Doing the validation directly inline allows the validation to pass without error.
There must be something I'm overlooking or doing wrong...
The validations worked fine when doing them directly in the model instead of within the conditional_reservation_validation.
Example code that causes SystemStackError stack level too deep
The basic process has been built following this example:
Building Partial Objects Step by Step
I know this needs to be refactored / cleaned up.
Snippet from model:
validate :conditional_reservation_validation
def conditional_reservation_validation
if created_by_admin?
validates_presence_of :vehicle_location
validates_presence_of :pickup, :dropoff, :departure_date, :departure_time, :passengers
else
if active_or_parking?
validates_presence_of :vehicle_location
# SystemStackError occurs when validate :parking_agreement_if_location fires
validate :parking_agreement_if_location
end
if active_or_pickup?
# additional validations
end
end
# ...
end
def parking_agreement_if_location
if vehicle_in_special_parking_location(vehicle_location)
if self.parking_agreement == true
# ok
else
self.errors.add :base, "Must read and understand parking instructions."
end
end
end
def vehicle_in_special_parking_location(vehicle_location)
parking_locations = Location.where(require_parking: true)
if parking_locations.include?(vehicle_location)
return true
else
return false
end
end
# methods to check the step in process
def active_or_parking?
status.include?('parking') || active?
end
Calling validate :parking_agreement_if_location triggers a SystemStackError
Example code that stops the error:
Just taking the code out of the :parking_agreement_if_location method and putting it directly inline stops the SystemStackError.
validate :conditional_reservation_validation
def conditional_reservation_validation
if created_by_admin?
validates_presence_of :vehicle_location
validates_presence_of :pickup, :dropoff, :departure_date, :departure_time, :passengers
else
if active_or_parking?
validates_presence_of :vehicle_location
if vehicle_location
locations = Location.where(require_parking: true)
if locations.include?(vehicle_location)
if parking_agreement == true
# ok
else
self.errors.add :base, "Must read and understand parking instructions."
end
# validate :parking_agreement_if_location
end
end
end
if active_or_pickup?
# These methods cause SystemStackError as well...
validate :pickup_date_in_range
validate :pickup_date_in_future
end
end
end
Controller update action:
def update
params[:reservation][:status] = step.to_s
params[:reservation][:status] = 'active' if step == steps.last
case step
when :parking
#reservation.assign_attributes(reservation_params)
when :pickup
#reservation.assign_attributes(reservation_params)
when :billing
#reservation.assign_attributes(reservation_params)
end
render_wizard #reservation
end
You are using validations wrong way. They need to be invoked on class level.
You need to use conditional validations instead:
validates_presence_of :vehicle_location, if: :created_by_admin?
validates_presence_of :pickup, :dropoff, :departure_date, :departure_time, :passengers, if: :created_by_admin?
validates_presence_of :vehicle_location, unless: :created_by_admin?, if: :active_or_parking?
validate :parking_agreement_if_location, unless: :created_by_admin?, if: :active_or_parking?
Thanks for answering my previous question, but I ran into a new problem.
I'm creating a custom validator that validates whether a user typed in a clean word. This
is used on my UsersController as a validation method.
I am using the Obscenity gem but created some of my own methods to ensure quality data.
Error Message
NoMethodError: Undefined method include? for Nil:NilClass
The problem with this is that my methods work if a record already exists, but they don't work during record creation. I've tried to combat this problem by using
:on => [:create, :update]
but I still receive the same error.
Validation Methods
class MyValidator < ActiveModel::Validator
def mystery_setup
#mystery_words = # This is a mystery, I can't tell you.
#mystery_c = #mystery_words.map(&:capitalize)
#mystery_u = #mystery_words.map(&:upcase)
#mysteries = #mystery_words + #mystery_c + #mystery_u
#new_mysteries = #mysteries.map{|mystery|mystery.tr("A-Za-z", "N-ZA-Mn-za-m")}
end
def validate (user)
mystery_setup
if Obscenity.profane?(user.name) \
|| #new_mysteries.any?{|mystery|user.name.include?(mystery)} \
|| #new_mysteries.any?{|mystery|user.email.include?(mystery)} \
|| #new_mysteries.any?{|mystery|user.password.include?(mystery)}
user.errors[:name] << 'Error: Please select a different username'
end
end
end
User.rb(Model)
class User < ActiveRecord::Base
include ActiveModel::Validations
validates_with MyValidator
has_many :favorites, foreign_key: "user_id", dependent: :destroy
has_many :pictures, through: :favorites
has_secure_password
before_create :create_remember_token
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates_presence_of :name, :password, :email
validates_uniqueness_of :name, :email
validates :name, length: { in: 3..20 }
validates :password, length: { minimum: 6 }
validates :email, format: { with: VALID_EMAIL_REGEX }, length: { in: 8..50 }
validates_confirmation_of :password, if: lambda { |m| m.password.present? }
def User.new_remember_token
SecureRandom.urlsafe_base64
end
def User.digest(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = User.digest(User.new_remember_token)
end
end
I have also tried using an unless statement
def validate (user)
mystery_setup
unless User.all.include?(user)
if (Obscenity.profane?(user.name)
|| #new_mysteries.any {|mystery|user.name.include?(mystery)}) \
|| #new_mysteries.any?{|mystery|user.email.include?(mystery)} \
|| #new_mysteries.any?{|mystery|user.password.include?(mystery)}
user.errors[:name] << 'Error: Please select a different username'
end
end
end
end
I tried testing if there was a user by using the unless statement but that didn't work either.
Following advice from a similar question here, I changed my migrations file to combat this area.
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, default: 'new'
t.string :password, default: 'new'
t.string :email, default: 'new'
t.timestamps
end
end
end
Question Link
undefined method `include?' for nil:NilClass with partial validation of wizard gem
Reference for Code
http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations
Changing the migration file by changing the default values didn't solve this question so I decided to ask a new question here.
This method works for updating records but not for creating new records.
Help is appreciated. Thank you in advanced.
Edit
Just received an excellent suggestion to pass in the attributes in bracket format. My code now looks like this
def validate (user)
mystery_setup
unless User.all.include?(user)
if (Obscenity.profane?(user[:name]) ||
#new_mysteries.any?{|mystery|user[:name].include?(mystery)}) \
||#new_mysteries.any?{|mystery|user[:email].include?(mystery)}
||#new_mysteries.any?{|mystery|user[:password].include?(mystery)}
user.errors[:name] << 'Error: Please select a different username'
end
end
end
Right now, it only has an error with the email and password attributes. If I delete the last two ||#new_mysteries.any? lines, my method works for filtering the name.
I would like to keep this professional though, so I'd like to get it to work with the other two methods. Possibly has to do with my use of parentheses or the || symbol?
Solid progress guys, keep it up.
Edit
Also, if I would like to call these validation methods on other classes, would it be better to put this in a helper file?
New Update
Here is my Users Controller code
class UsersController < ApplicationController
before_action :signed_in_user, only: [:edit, :update, :destroy]
before_action :correct_user, only: [:edit, :update]
def index
#users = User.all
end
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
flash[:success] = "Congratulations #{#user.name}! You have successfully created an account"
redirect_to games_path
else
render 'new'
end
end
def edit
end
def update
#user = User.find(params[:id])
end
def favorites
#user = User.find(current_user)
end
def destroy
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url notice: "Please sign in."
end
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
end
You could write that like this:
def validate (user)
mystery_setup
user.errors[:name] << 'Tsk! Tsk! Please select a different username' if
Obscenity.profane?(user[:name]) ||
[:name, :email, :password].product(#new_mysteries).any? { |sym, mystery|
(str = user.public_send sym) && str.include?(mystery) }
end
Thanks to #Arup for the fix.
If you wished to reduce the number of instance variables, you could change the first line to:
new_mysteries = mystery_setup
and change #new_mysteries to new_mysteries.
|| #new_mysteries.any?{|mystery|user.name.include?(mystery)} \
|| #new_mysteries.any?{|mystery|user.email.include?(mystery)} \
|| #new_mysteries.any?{|mystery|user.password.include?(mystery)}
This error means that user name, email or password is nil. To deal with it you need to change each line to:
user.name && user.name.include?(mystery)
However highly recommend andand gem, which will allow you to write the above as:
user.name.andand.include?(mystery)
try this out:
def validate (user)
mystery_setup
unless User.all.include?(user)
if user.name && user.email && user.password
if (Obscenity.profane?(user.name)
|| #new_mysteries.any {|mystery|user.name.include?(mystery)}) \
|| #new_mysteries.any?{|mystery|user.email.include?(mystery)} \
|| #new_mysteries.any?{|mystery|user.password.include?(mystery)}
user.errors[:name] << 'Error: Please select a different username'
end
end
end
end
end
class MyValidator < ActiveModel::Validator
def mystery_setup
mystery_words = # This is a mystery, I can't tell you.
mystery_c = mystery_words.map(&:capitalize)
mystery_u = mystery_words.map(&:upcase)
mysteries = mystery_words + mystery_c + mystery_u
mysteries.map{ |mystery| mystery.tr("A-Za-z", "N-ZA-Mn-za-m")}
end
def validate (user)
# No need to pollute the class with instance variables, just pass it back in a return
new_mysteries = mystery_setup
if Obscenity.profane?(user.name.to_s) ||
#new_mysteries.any?{ |mystery| user.name.to_s.include?(mystery) ||
user.email.to_s.include?(mystery) ||
user.password.to_s.include?(mystery)}
user.errors[:name] << 'Error: Please select a different username'
end
end
end
I have refactored the code a bit, let me know if this works:
def validate (user)
mystery_setup
if Obscenity.profane?(user.name)
user.errors[:name] << 'Error: Please select a different username'
end
%w(name email password).each do |attr|
value = user.send(attr)
if value.present? and #new_mysteries.grep(/#{value}/).present?
user.errors[attr] << "Error: Please select a different user#{attr}"
end
end
end
You have an error in this part, the first line.
#mystery_words = # This is a mystery, I can't tell you.
#mystery_c = mystery_words.map(&:capitalize)
This should be
#mystery_words = [] # This is a mystery, I can't tell you.
#mystery_c = mystery_words.map(&:capitalize)
Since nil is the representation of atomic nothingness in Ruby, it never includes anything. The include? method can be simply defined on it as:
def nil.include? arg
false
end
So I want to do this because I think it is the most idiomatic way to do errors.
For example:
User < ActiveRecord::Base
def add_email?
...
#in the case that it does have an error
MyErrorObjectThatEvaluatesToFalse.new("The email is already taken")
end
def is_valid_user?
...
MyErrorObjectThatEvaluatesToFalse.new("Username is not set")
...
end
end
...
SomeController
...
if error = user.add_email?
render_error_msg(error.message) and return
elsif error = user.is_valid_user?
render_error_msg(error.message) and return
end
...
end
I've tried one of the solutions below, but it doesn't have the functionality that I would like:
class A
def ==(comp)
false
end
end
a = A.new
if a
puts "'a' evaluated to true"
else
puts "'a' evaluated to false"
end
#=> 'a' evaluated to true
Is there a way to do something like this or has some else found a way to handle errors that is better than the current rails way of indirectly getting the message with a combination of user.valid? and user.errors?
Thanks!
I would not recommend this as a method of validation, however to define a class that returns false on a comparator:
class A
def ==(comp)
false
end
end
A.new == "a" #false
A.new == true #false
A.new == false #false
A.new == nil #false
I would recommend using rails' built in validations.
class User < ActiveRecord::Base
validates :username, :presence => true
end
user = User.new
user.errors #["username must not be blank"]