create calls before_save multiple times - ruby-on-rails

I have a before_save callback in my model which encrypts 2 fields before they're saved to the database.
class Account < ActiveRecord::Base
before_save :encrypt_credentials, if: "!username.blank? && !password.blank?"
def encrypt_credentials
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end
The situation is very similar to Devise models run before_save multiple times?. When I call Model.create!(...) - which includes the 2 fields that need to be encrypted, the before_save gets called twice, ending up in the fields being encrypted twice.
Account.create!(
{
username: ENV['USERNAME'],
password: ENV['PASSWORD']
})
Why is before_save called multiple times? I don't like the solution of the post linked above and I don't want to do new/build followed by save.

It was user error :( After calling account = Account.create!, I had other code which called save! back on the model: account.foo = bar; account.save!. This obviously called befor_save again and re-encrypted my fields. I ended up with something like this:
class Account < ActiveRecord::Base
before_save :encrypt_username, if: :username_changed?
before_save :encrypt_password, if: :password_changed?
def encrypt_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.username = crypt.encrypt_and_sign(username)
end
def encrypt_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
self.password = crypt.encrypt_and_sign(password)
end
def decrypted_username
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(username)
end
def decrypted_password
crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
crypt.decrypt_and_verify(password)
end
end

Option 1 (could be a mistake in usage of callbacks):
Short answer: use after_save instead of before_save
Long answer: How to organize complex callbacks in Rails?
When you use the:
account = Account.new
account.save
You are firing the before_save hook each time.
Option 2 (could be a bug):
Maybe you're actually touching the record several times.
For example in:
def create
#account = Customer.find(params[:customer_id]).accounts.create(account_params)
if #account.save
redirect_to customer_account_path(#account.customer.id, #account.id)
else
render :new
end
end
You are in fact touching it with create and save. In which case I suggest:
def create
#account = Customer.find(params[:customer_id]).accounts.build(account_params)
if #account.save
redirect_to customer_account_path(#account.customer.id, #account.id)
else
render :new
end
end
Build doesn't try to save the record so you shouldn't have any more problems. Hope this helps! Have a great day!

Related

How to use before_save (with params) to check if a record exist

I would like to check if a record exist before_save, what's the best way tod do that ?
def create
#step = Step.new(step_params)
#course = Course.find(step_params[:course_id])
redirect_to course_path(#course) and return if step_already_present?(#step)
if #step.save
redirect_to course_path(#course.id)
else
render :new
end
end
The method to check :
def step_already_present?(step)
Step.where(poi_start_id: step.poi_start_id, course_id: step.course_id).first.present?
end
You can use the uniqueness validation on the Model
If you need to check the two columns together you can use the scope option like this:
class Step < ActiveRecord::Base
validates :poi_start_id, uniqueness: { scope: :course_id }
end

Devise not confirming email on update

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

How does .valid? run even there is no object in front of it (valid? instead of object.valid?)?

Watching Railscasts, more specifically Form Objects. Here is the code.
Controller code:
def create
#signup_form = SignupForm.new
if #signup_form.submit(params[:user])
session[:user_id] = #signup_form.user.id
redirect_to #signup_form.user, notice: "Thank you for signing up!"
else
render "new"
end
end
Method found on form object:
class SignupForm
def submit(params)
user.attributes = params.slice(:username, :email, :password, :password_confirmation)
profile.attributes = params.slice(:twitter_name, :github_name, :bio)
self.subscribed = params[:subscribed]
if valid?
generate_token
user.save!
profile.save!
true
else
false
end
end
end
I understand most of the code, but what I don't understand is how .valid? can run without an object written directly in front of it (i.e.: object.valid?)? I tried replicating this with Ruby, but Ruby requires an object to be directly written in front of the method, which leads me to believe this is some sort of Rails magic.
Can someone explain how .valid? runs without an object in front of it , and which object it picks up?
I tried using the following Ruby code and did not work:
array = [1,2,3,4]
def meth
if is_a?
puts "is array"
else
puts "not array"
end
end
array.meth => error: wrong number of arguments (given 0, expected 1)
In the Railscast #416 in question, Ryan includes (among others) the ActiveModel::Validations module into the SignupForm class. This module implements the valid? method for the class.
Now, in Ruby you can always call methods on the current object (i.e. self) without explicitly naming the receiver. If the receiver is unnamed, self is always assumed. Thus, in your submit method, valid? in called on the same instance of the SubmitForm where you originally called submit on.
class SignupForm
def submit(params)
user.attributes = params.slice(:username, :email, :password, :password_confirmation)
profile.attributes = params.slice(:twitter_name, :github_name, :bio)
self.subscribed = params[:subscribed]
if valid?
generate_token
user.save!
profile.save!
true
else
false
end
end
def valid? // <--- this is what they are calling.
return true // this is made up... i am sure it does something
end
end

create rails nested object in one swoop

I need an api key to save a user, and I need a user_id to save an api_key... Can I do both at once?
user.api_key = ApkiKey.generate_token
user.save
user.api_key.user_id = user.id
user.api_key.save
If the api_key has belongs_to relationship with user then following will work
user.api_key = ApkiKey.generate_token
user.api_key.user_id = user.id
user.save
the user.save will also trigger the user.api_key.save
I ended up doing the following:
#api_key.rb
before_create :generate_access_token
def generate_access_token
begin
self.access_token = SecureRandom.hex
end while self.class.exists?(access_token: access_token)
end
#user.rb
before_create do |user|
user.api_key = ApiKey.create(user_id: user.id)
end
The problem was that I didn't think I could access user.id before I created the user, but apparently it works. Thanks for the heads up #Hardik

Undefined method include? for Nil:NilClass

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

Resources